泛型
1.什么是泛型?
Java是一种强类型语言,数据类型在编译时必须确定。如果我们想要在代码中使用不同类型的数据,那么就需要为每种类型分别写出相应的代码。这样会导致代码冗长、重复,也不便于维护。为了解决这个问题,Java引入了泛型机制,允许在类、接口、方法中使用类型参数,使代码更加通用、简洁、安全。
示例代码:
// 不使用泛型
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // 需要强制类型转换,容易出现类型转换异常
// 使用泛型
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 不需要强制类型转换,类型安全
2.泛型类如何定义使用?
泛型类定义的语法格式如下:
public class 类名<类型参数列表> {
// 成员变量、方法等
}
其中,类型参数列表是用尖括号包围起来的一组类型参数,可以有多个,用逗号隔开。类型参数可以在类中的任何位置使用。泛型类的实例化时需要指定类型实参。
示例代码:
public class ClassName<T> {
private T value;
public ClassName(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
ClassName<Integer> obj = new ClassName<Integer>(123);
int value = obj.getValue();
3.泛型接口如何定义使用?
泛型接口定义的语法格式如下:
public interface 接口名<类型参数列表> {
// 方法声明
}
其中,类型参数列表和泛型类的类型参数列表语法相同,可以在接口中的任何位置使用。泛型接口的实现类需要指定类型实参。
public interface InterfaceName<T> {
void doSomething(T parameter);
}
InterfaceName<String> obj = new InterfaceName<String>() {
public void doSomething(String parameter) {
System.out.println(parameter);
}
};
obj.doSomething("hello");
4.泛型方法如何定义使用?
泛型方法定义的语法格式如下:
修饰符 <类型参数列表> 返回值类型 方法名(形参列表) {
// 方法体
}
其中,类型参数列表和泛型类、泛型接口的类型参数列表语法相同,可以在方法中的任何位置使用。在方法调用时,可以显式指定类型实参,也可以通过参数类型推断省略类型实参。
public class ClassName {
public <T> void methodName(T parameter) {
System.out.println(parameter);
}
}
ClassName obj = new ClassName();
obj.<String>methodName("hello");
5.泛型的上限和下限?
泛型的上限和下限是限制类型参数的边界。泛型的上限使用 extends 关键字,表示类型参数必须是指定的类型或其子类。泛型的下限使用 super 关键字,表示类型参数必须是指定的类型或其超类。
// 上限
public class ClassName {
public <T extends Number> void methodName(T parameter) {
System.out.println(parameter);
}
}
ClassName obj = new ClassName();
obj.methodName(123); // OK
obj.methodName("hello"); // 编译错误,String不是Number的子类
// 下限
public void methodName(List<? super Integer> parameter) {
parameter.add(123);
parameter.add(3.14); // 编译错误,3.14是double类型
}
List<Number> list1 = new ArrayList<Number>();
methodName(list1); // OK
List<Object> list2 = new ArrayList<Object>();
methodName(list2); // 编译错误,Object不是Integer或Integer的超类
6.如何理解Java中的泛型是伪泛型?
Java中的泛型是通过类型擦除实现的,即在编译时擦除类型信息,在运行时不保留类型信息。因此,在运行时无法获得泛型类型参数的具体类型。这样,Java中的泛型就成了一种“伪泛型”。泛型类型信息在编译时可以检查,但在运行时被擦除,这是泛型机制的一个局限性,但也是Java泛型的一个优点,因为这种方式避免了运行时的类型转换错误,并提高了运行时的性能。
另外,Java中的泛型还存在一个“类型擦除”问题。在使用泛型时,类型参数实际上被转换成了其边界类型或 Object 类型。这就导致了一些限制,比如无法创建泛型数组,无法在泛型类型中使用基本类型等。虽然Java中的泛型有一些限制,但仍然是一种非常有用的机制,可以使代码更加通用、简洁、安全。
public class ClassName<T> {
private T value;
public ClassName(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
ClassName<Integer> obj1 = new ClassName<Integer>(123);
ClassName<String> obj2 = new ClassName<String>("hello");
System.out.println(obj1.getClass().getName()); // ClassName
System.out.println(obj2.getClass().getName()); // ClassName
// 泛型类型信息在运行时被擦除
异常
1.Java异常类层次结构?
Java异常类层次结构是异常处理的基础,如下所示:
java.lang.Object
|
+--java.lang.Throwable
|
+--java.lang.Error
|
+--java.lang.Exception
|
+--java.io.IOException
|
+--java.sql.SQLException
|
+--其他异常类
所有的异常类都继承自 Throwable 类。Throwable 类有两个直接子类:Error 和 Exception。Error 表示系统级别的错误和资源耗尽等无法恢复的错误,程序通常无法处理。例如,OutOfMemoryError 表示内存不足错误。Exception 是程序中可能会遇到的异常情况,程序可以通过捕获和处理这些异常来恢复正常运行。Exception 又分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)。可查的异常是指在编译时必须进行处理的异常,这些异常必须被捕获或声明抛出。不可查的异常是指在编译时无法确定是否需要处理的异常,这些异常不需要被捕获或声明抛出。
2.可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)区别?
可查的异常必须在编译时进行处理,否则编译器会报错。这些异常通常是由系统或程序逻辑错误引起的,例如文件不存在、输入输出错误等。常见的可查异常包括 IOException、SQLException 等。
不可查的异常通常是由程序设计错误引起的,例如空指针异常、类型转换异常等。这些异常通常发生在运行时,编译器无法检测到。不可查的异常不需要在方法签名中声明抛出,并且不需要在调用时进行处理。
3.throw和throws的区别?
throw 和 throws 关键字都用于处理异常,但含义不同。throw 关键字用于抛出一个异常对象。在方法中,当程序发现异常情况时,可以使用 throw 关键字抛出一个异常对象。例如:
if (x == null) {
throw new NullPointerException("x is null");
}
throws 关键字用于在方法签名中声明可能抛出的异常。在方法中,如果可能会抛出一个异常,但是不想在方法内部进行处理,可以在方法签名中使用 throws 关键字声明可能抛出的异常。例如:
public void foo() throws Exception {
// 方法体
}
4.Java 7 的 try-with-resources?
Java 7 引入了 try-with-resources 语句,这是一种简化关闭资源代码的方法。在 try-with-resources 语句中,可以自动关闭实现了 java.lang.AutoCloseable 接口的资源,例如文件或数据库连接等。
以下是一个使用 try-with-resources 语句的简单示例:
public class Demo {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上面的示例中,BufferedReader 和 FileReader 都实现了 AutoCloseable 接口。在 try-with-resources 语句中,我们创建一个 BufferedReader 对象和一个 FileReader 对象,并将它们作为资源传递给 try 语句。当程序离开 try 块时,JVM 会自动关闭这两个资源。
try-with-resources 语句可以有多个资源,例如:
try (Resource1 res1 = new Resource1(); Resource2 res2 = new Resource2()) {
// ...
}
在这种情况下,两个资源都会被自动关闭。如果资源之间有依赖关系,可以按照需要在 try-with-resources 语句中创建和关闭资源。
5.Java 异常的底层实现,以及Exception Table是什么?
Java 异常的底层实现涉及到 Java 虚拟机(JVM)中的 Exception Table。在 Java 代码编译成字节码时,编译器会将所有可能抛出异常的代码块标记为异常处理器。这些异常处理器被编译器放在 Exception Table 中,并包含了异常处理器的入口地址、代码块的起始和结束位置、以及异常类型等信息。
当程序执行到可能抛出异常的代码块时,JVM 会检查 Exception Table 中是否存在与当前抛出的异常匹配的异常处理器。如果存在,则将程序控制流转移到对应的异常处理器,执行异常处理器中的代码。如果不存在匹配的异常处理器,则将异常抛出到调用栈上层的异常处理器或者 JVM 运行时系统中进行处理。
下面是一个简单的示例,用于演示 Exception Table 的实现:
public class Demo {
public static void main(String[] args) {
int result = divide(10, 0);
System.out.println("Result: " + result);
}
public static int divide(int a, int b) {
int result = 0;
try {
result = a / b;
} catch (ArithmeticException e) {
System.out.println("Exception caught: " + e.getMessage());
}
return result;
}
}
在上面的示例中,divide() 方法计算两个整数的商。如果除数为 0,则会抛出一个 ArithmeticException 异常。在 main() 方法中,调用 divide() 方法计算结果,并打印计算结果。
当执行到 result = a / b 语句时,JVM 会检查 Exception Table 中是否存在匹配 ArithmeticException 异常的异常处理器。在这个示例中,存在一个匹配的异常处理器,它位于 catch (ArithmeticException e) 代码块中。JVM 将程序控制流转移到异常处理器中,并执行其中的代码,打印异常信息。然后,JVM 将程序控制流返回到调用 divide() 方法的地方,继续执行程序。
反射
什么是反射?
反射是一种在运行时动态地获取类的信息和调用类的方法的能力。Java 反射 API 可以让程序在运行时动态地创建对象、访问对象属性、调用对象方法以及获取对象信息等,从而增强了程序的灵活性和可扩展性。
2.反射的使用?
反射可以用来实现一些高级的特性,比如依赖注入、框架配置和代理等。反射 API 主要提供了以下几个类来实现反射功能:
Class 类:表示一个类的类型,可以用来获取类的信息、实例化对象、访问类的静态成员和调用类的方法等。
Field 类:表示一个类的字段,可以用来访问和修改对象的属性。
Method 类:表示一个类的方法,可以用来调用对象的方法。
Constructor 类:表示一个类的构造函数,可以用来实例化对象。
下面是一个简单的示例,用于演示反射的使用:
public class Demo {
private String message;
public Demo(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("Demo");
Constructor<?> constructor = cls.getConstructor(String.class);
Object obj = constructor.newInstance("Hello, world!");
Method method = cls.getMethod("getMessage");
String message = (String) method.invoke(obj);
System.out.println(message);
}
}
在上面的示例中,Demo 类有一个带有一个参数的构造函数和一个返回 message 属性值的 getMessage() 方法。在 Main 类中,使用 Class.forName() 方法获取 Demo 类的类型,然后使用 getConstructor() 方法获取 Demo 类的带有一个字符串参数的构造函数。接着,使用 newInstance() 方法创建一个 Demo 类的实例。最后,使用 getMethod() 方法获取 Demo 类的 getMessage() 方法,然后使用 invoke() 方法调用该方法,并将返回值强制转换为字符串类型。
3.getName、getCanonicalName与getSimpleName的区别?
这三个方法都是在 Class 类中定义的,可以用来获取一个类的名称或规范化名称。在很多情况下,它们的返回值是相同的。但是,在一些特殊情况下,它们的返回值可能会有所不同。
getName() 方法返回的是一个类的全限定名,包括包名和类名,例如 "java.lang.String"。
getCanonicalName() 方法返回的是该类的规范化名称,也就是在所有类型名称上执行一系列规则后的名称,例如 "java.lang.String"。
getSimpleName() 方法返回的是该类的简单名称,也就是不包括包名的类名,例如 "String"。