Java 泛型中级面试题及答案
Java 泛型中级面试题及答案
1. 泛型擦除的原理及对运行时的影响
答案
泛型擦除是Java编译器在编译阶段将泛型类型信息移除的过程,使得生成的字节码中仅保留原始类型。这一机制主要是为了保持Java的向后兼容性。在运行时,由于泛型类型信息被擦除,会导致以下限制:
- 对象创建限制:不能直接使用泛型类型参数创建对象,例如
new T()
这种写法是不允许的。因为在运行时,泛型类型参数T
已被擦除,编译器无法确定要创建的具体对象类型。 - 数组初始化限制:无法直接进行泛型数组初始化。这是因为数组在运行时需要知道其元素的确切类型来进行类型检查,而泛型擦除后无法提供这种确切类型信息。
- 类型判断限制:不能用泛型类型参数进行
instanceof
判断。因为运行时泛型类型信息不存在,无法准确判断对象是否属于某个泛型类型。
代码示例
public class ErasureDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("Hello");// 编译错误:无法创建泛型数组// T[] arr = new T[10];// 反射绕过泛型检查try {list.getClass().getMethod("add", Object.class).invoke(list, 123);System.out.println(list); // 输出:[Hello, 123]} catch (Exception e) {e.printStackTrace();}}
}
在上述代码中,首先创建了一个 List<String>
并添加了字符串元素。尝试创建泛型数组会导致编译错误,体现了泛型数组初始化的限制。通过反射绕过泛型检查,向 List<String>
中添加了一个 Integer
类型元素,虽然编译通过,但运行时可能引发 ClassCastException
,展示了泛型擦除后可能出现的类型安全问题。
2. 泛型方法与类型推断
答案
泛型方法通过在方法声明中使用 <T>
等形式声明类型参数,类型推断则允许编译器在调用泛型方法时自动确定实际类型。泛型方法的返回值类型通常需要包含泛型参数,并且类型参数可以在参数列表中使用,以增强方法的通用性。
代码示例
public class GenericMethod {// 类型推断示例public static <T> void print(T value) {System.out.println("Value: " + value);}// 多类型参数示例public static <K, V> void put(K key, V value) {System.out.println("Key: " + key + ", Value: " + value);}public static void main(String[] args) {print("Hello"); // 自动推断为Stringprint(123); // 自动推断为Integerput("name", "Alice");put(1001, "Java");}
}
在这个示例中,print
方法展示了类型推断的功能,编译器根据传入的参数类型自动确定泛型参数 T
的实际类型。put
方法则展示了多类型参数的泛型方法,通过不同的类型参数 K
和 V
来处理不同类型的键值对。
3. 通配符 <?>
与 <? extends T>
的区别
答案
<?>
(无界通配符):表示可以接受任何类型,它用于在对集合元素类型完全未知的情况下进行操作。<? extends T>
(上界通配符):表示可以接受T
类型及其子类类型,用于限制集合只能操作T
及其子类类型的元素。
使用场景
<?>
的场景:当你只需要对集合进行读取操作,且不关心集合中元素的具体类型时,使用无界通配符。例如,简单地打印集合中的所有元素。<? extends T>
的场景:当你需要读取集合中的元素,并且希望确保这些元素是特定类型T
或其子类时,使用上界通配符。比如计算一组数字(Number
及其子类)的总和。
代码示例
import java.util.Arrays;
import java.util.List;public class WildcardDemo {public static void printList(List<?> list) {for (Object obj : list) {System.out.println(obj);}}public static double sumNumbers(List<? extends Number> numbers) {double total = 0;for (Number num : numbers) {total += num.doubleValue();}return total;}public static void main(String[] args) {List<Integer> intList = Arrays.asList(1, 2, 3);printList(intList);System.out.println("Sum: " + sumNumbers(intList));}
}
printList
方法使用无界通配符 <?>
,可以接受任何类型的列表并打印其元素。sumNumbers
方法使用上界通配符 <? extends Number>
,只能接受 Number
及其子类类型的列表,并计算其总和。
4. 泛型类与泛型方法的优先级
答案
当泛型类和泛型方法同时存在时,编译器优先使用方法级泛型参数。如果方法没有定义泛型参数,那么编译器会使用类的泛型参数。
代码示例
public class GenericPriority<T> {public void print(T value) {System.out.println("Class Generic: " + value);}public <T> void print(T value) {System.out.println("Method Generic: " + value);}public static void main(String[] args) {GenericPriority<String> gp = new GenericPriority<>();gp.print("Hello"); // 调用方法级泛型gp.print(123); // 编译错误(类泛型为String)}
}
在这个示例中,GenericPriority
类定义了泛型参数 T
,同时类中又有一个与类泛型参数同名但独立的泛型方法 print
。在调用 print
方法时,编译器优先使用方法级泛型参数,所以 gp.print("Hello")
调用的是方法级泛型。而 gp.print(123)
由于类泛型为 String
,与传入的 Integer
类型不匹配,会导致编译错误。
5. 泛型与数组的兼容性问题
答案
Java不允许直接创建泛型数组,因为泛型擦除会导致运行时无法确定数组元素的确切类型,从而引发潜在的类型安全问题。泛型数组在运行时其实际类型为 Object[]
,这可能会使程序在运行时出现 ClassCastException
。不过,可以通过反射或者使用 List
来间接实现类似泛型数组的功能。
解决方案代码示例
public class GenericArray<T> {private T[] array;@SuppressWarnings("unchecked")public GenericArray(int size) {array = (T[]) new Object[size]; // 强制转换绕过限制}public void set(int index, T value) {array[index] = value;}public T get(int index) {return array[index];}public static void main(String[] args) {GenericArray<Integer> arr = new GenericArray<>(3);arr.set(0, 10);arr.set(1, 20);System.out.println(arr.get(0)); // 输出:10}
}
在上述代码中,GenericArray
类通过在构造函数中使用强制类型转换 (T[]) new Object[size]
来绕过无法直接创建泛型数组的限制。但这种方式需要手动保证类型安全,因为运行时数组实际类型为 Object[]
。如果不小心向数组中添加了不匹配的类型,运行时可能会抛出 ClassCastException
。
6. 泛型在集合框架中的应用
答案
Java集合框架广泛应用泛型来确保类型安全。例如 List<E>
、Map<K, V>
等接口,通过泛型参数 E
、K
、V
来指定集合中元素或键值对的类型。使用泛型后,集合在编译期就能进行类型检查,避免运行时出现 ClassCastException
,提高了代码的可靠性和可读性。
代码示例
import java.util.ArrayList;
import java.util.List;public class CollectionGeneric {public static void main(String[] args) {List<String> names = new ArrayList<>();names.add("Alice");names.add("Bob");// 编译错误:类型不匹配// names.add(123);for (String name : names) {System.out.println(name.length());}}
}
在这个例子中,创建了一个 List<String>
集合,只能添加 String
类型的元素。如果尝试添加 Integer
类型元素(如 names.add(123)
),会导致编译错误,从而在编译阶段就发现类型不匹配问题。通过使用泛型,增强了集合操作的类型安全性。
7. 泛型与继承的关系
答案
泛型类型之间不存在继承关系,例如 List<String>
不是 List<Object>
的子类型。然而,可以通过通配符来实现有限制的继承关系。比如 List<? extends Object>
是合法的,表示可以接受 Object
及其子类类型的列表;但直接将 List<String>
赋值给 List<Object>
会导致编译错误。
代码示例
import java.util.ArrayList;
import java.util.List;public class GenericInheritance {public static void process(List<Object> list) {list.add("Test");}public static void main(String[] args) {List<String> strList = new ArrayList<>();// 编译错误:不兼容的类型// process(strList);// 使用通配符实现多态process(new ArrayList<Object>());}
}
在上述代码中,process
方法接受 List<Object>
类型的参数。当尝试将 List<String>
传递给该方法时,会出现编译错误,因为 List<String>
不是 List<Object>
的子类型。但通过使用通配符,如 process(new ArrayList<Object>())
,可以实现类似多态的效果,将符合 List<Object>
要求的列表传递给方法。