一、类型擦除(Type Erasure)
Q1:什么是 Java 泛型的类型擦除?
答: Java 泛型是编译期概念,编译后泛型信息会被擦除,运行时不存在泛型。具体表现为:
无界类型
T被擦除为Object有界类型
T extends Number被擦除为Number泛型参数会被替换为原始类型(raw type)
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true,都是 java.util.ArrayListQ2:类型擦除后,编译器做了哪些补偿?
答: 主要做两件事:
插入强制类型转换(在取值时)
生成桥接方法(Bridge Method,用于保证多态)
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 编译后实际是 (String) list.get(0)桥接方法示例:
class Node<T> {
public void set(T t) {}
}
class MyNode extends Node<String> {
@Override
public void set(String s) {} // 编译后会生成一个 set(Object) 桥接方法
}Q3:为什么 Java 要使用类型擦除?
答: 核心原因是向后兼容。Java 5 引入泛型时,必须保证:
老版本的非泛型代码(如
List)仍能运行泛型代码和非泛型代码能互相调用
如果采用"具体化泛型"(如 C# 那样运行时保留),字节码格式和老代码都要大改,成本过高。
Q4:类型擦除带来了哪些限制?
答: 常见限制有:
// 编译错误:name clash
void method(List<String> list) {}
void method(List<Integer> list) {}Q5:如何绕过"不能 new T()"的限制?
答: 通过传入 Class<T> 对象,利用反射创建:
public static <T> T create(Class<T> clazz)
throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}或者使用 Supplier<T>:
public static <T> T create(Supplier<T> supplier) {
return supplier.get();
}Q6:类型擦除下,List<String> 和 List<Integer> 能通过反射互相添加元素吗?
答: 可以。因为运行时泛型被擦除,反射绕过了编译期检查:
List<Integer> list = new ArrayList<>();
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list, "字符串"); // 成功
System.out.println(list); // [字符串]这也是"泛型只是编译期糖"的经典证明。
二、通配符 extends / super
Q7:<? extends T> 和 <? super T> 分别代表什么?
答:
<? extends T>:上界通配符,表示类型是 T 或 T 的子类<? super T>:下界通配符,表示类型是 T 或 T 的父类
List<? extends Number> list1; // 可以是 List<Integer>、List<Double>
List<? super Integer> list2; // 可以是 List<Integer>、List<Number>、List<Object>Q8:为什么 List<? extends Number> 不能 add 元素?
答: 因为编译器只知道它是 Number 的某个未知子类,无法确定具体类型。
List<? extends Number> list = new ArrayList<Integer>();
list.add(1); // 编译错误
list.add(1.0); // 编译错误
Number n = list.get(0); // 可以读,返回值至少是 Number如果允许添加,就可能把 Double 放进 List<Integer>,破坏类型安全。
结论:上界通配符只能读,不能写。
Q9:为什么 List<? super Integer> 可以 add,但 get 只能得到 Object?
答:
List<? super Integer> list = new ArrayList<Number>();
list.add(1); // 可以,Integer 一定是 Integer 或其父类
Object o = list.get(0); // 只能保证是 Object因为 list 可能是 List<Number> 或 List<Object>,读出来的类型无法确定为 Integer,只能当 Object。
结论:下界通配符可以写,但读只能读成 Object。
Q10:<?> 和 <? extends Object> 有什么区别?
答: 本质上几乎等价,都是无界通配符,但语义上:
<?>表示"任意类型",更简洁<? extends Object>显式写出上界
两者都只能读成 Object,不能写入(除 null 外)。
List<?> list = new ArrayList<String>();
list.add(null); // 唯一允许
Object o = list.get(0);Q11:下面代码为什么编译不通过?
List<? extends Number> list = new ArrayList<Integer>();
list.add(1);答: 因为 list 的静态类型是 List<? extends Number>,编译器不知道它实际是 List<Integer> 还是 List<Double>。若允许 add(1),就可能往 List<Double> 里放 Integer,破坏类型安全。
这就是"上界通配符不能写"的典型场景。
三、PECS 原则
Q12:什么是 PECS 原则?
答: PECS = Producer Extends, Consumer Super
由 Joshua Bloch 在《Effective Java》中提出,用于指导通配符的选择:
如果参数是生产者(只读取数据),用
<? extends T>如果参数是消费者(只写入数据),用
<? super T>如果既读又写,则不要用通配符
Q13:请用代码说明 PECS 的应用
答:
// 生产者:只从 src 读,用 extends
public static <T> void copy(List<? super T> dest,
List<? extends T> src) {
for (T t : src) { // src 是生产者
dest.add(t); // dest 是消费者
}
}调用:
List<Number> dest = new ArrayList<>();
List<Integer> src = List.of(1, 2, 3);
copy(dest, src); // 正确这是 JDK Collections.copy 的原型。
Q14:为什么 Collections.copy 的第一个参数是 List<? super T>?
答:
public static <T> void copy(List<? super T> dest,
List<? extends T> src)src只被读取(生产者)→extendsdest只被写入(消费者)→super
这样 copy(List<Number>, List<Integer>) 才能合法,否则如果写成 List<T> dest,就无法把 List<Integer> 复制到 List<Number>。
Q15:Collections.max 的签名是什么?为什么这样设计?
答:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)Collection<? extends T>:集合是生产者,只读取元素Comparable<? super T>:比较器是消费者,T 可以调用父类的 compareTo
这是 PECS 在 JDK 源码中的经典体现。
Q16:如果既读又写,应该怎么做?
答: 不要用通配符,直接用具体泛型类型:
// 不要这样
public void process(List<?> list) {
list.add(list.get(0)); // 编译错误
}
// 应该这样
public <T> void process(List<T> list) {
list.add(list.get(0)); // 正确
}四、综合与进阶
Q17:List<Object> 和 List<?> 有什么区别?
答:
List<Object> l1 = new ArrayList<String>(); // 编译错误
List<?> l2 = new ArrayList<String>(); // 正确Q18:List<? extends Number> 能赋值给 List<Number> 吗?
答: 不能。
List<? extends Number> a = new ArrayList<Integer>();
List<Number> b = a; // 编译错误因为 a 可能是 List<Integer>,如果赋给 List<Number>,就能往里面 add Double,破坏类型安全。
Q19:什么是"协变"和"逆变"?Java 数组和泛型有何不同?
答:
协变:
Integer[]是Number[]的子类型(Java 数组支持)逆变:
List<Number>能接收List<Object>(Java 泛型通过super模拟)不变:
List<Integer>不是List<Number>的子类型(Java 泛型默认)
Number[] arr = new Integer[10]; // 协变,编译通过
arr[0] = 1.0; // 运行时 ArrayStoreException
List<Number> list = new ArrayList<Integer>(); // 编译错误,泛型不变数组的协变是"不安全的",泛型的不变性正是为了修复这个缺陷。
Q20:如何在方法中同时使用 PECS 和类型擦除的知识?
答: 典型例子——写一个通用的合并方法:
public static <T> void merge(List<? super T> dest,
List<? extends T> src) {
for (T t : src) {
dest.add(t);
}
}src是生产者 →extendsdest是消费者 →super编译后 T 被擦除为 Object(无界)或上界类型
桥接方法保证多态调用
评论