泛型基础
Java 初级 · 进阶基础
泛型让集合与工具类在编译期就锁定元素类型,避免强制转型和运行时 ClassCastException。但 Java 的泛型有个关键特性——类型擦除(type erasure),不理解它你就会对「为什么 List<int> 不合法」「为什么不能 new T[]」「泛型数组怎么回事」一头雾水。本节把泛型的用法、擦除机制、桥方法、通配符 PECS 一并讲透。
一、为什么需要泛型
没有泛型的时代,List 存 Object,取出来要强转,出错只能运行时才发现:
// 无泛型(裸类型)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 若有人误 add(new Integer(1)),这里运行时 ClassCastException泛型把「类型检查」提前到编译期:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 无需强转,编译器保证里面一定是 String
// list.add(123); // 编译期直接报错,阻断 bug二、泛型类与泛型方法
class Box<T> { // T 是类型参数(占位符)
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> box = new Box<>();
box.set("hello");
String s = box.get(); // 编译期确定 T=String,无需强转泛型方法:类型参数写在返回类型前,由调用处推断:
static <T> void printArray(T[] arr) {
for (T e : arr) System.out.println(e);
}
printArray(new String[]{"a", "b"}); // T 推断为 String
printArray(new Integer[]{1, 2}); // T 推断为 Integer三、类型擦除:泛型的「真面目」(核心原理)
Java 的泛型是编译期语法糖——泛型信息只存在于源码和编译阶段,运行时被擦除。编译后,Box<String> 和 Box<Integer> 在 JVM 看来是同一个类 Box,类型参数被替换成它的「上限」(没指定上限就是 Object)。
用 javap 看编译后的 Box:
// 源码 Box<T> { T value; T get(); }
// 编译后(擦除为 Object):
java.lang.Object value;
java.lang.Object get(); // 返回 Object,编译器在调用处自动插入 (String) 强转擦除带来的限制(及其原因)
| 限制 | 原因 |
|---|---|
List<int> 非法,必须 List<Integer> | 基本类型不是 Object 子类,擦除后 T→Object 装不下 int;靠自动装箱解决 |
不能 new T[] / new T() | 运行时 T 已擦除,JVM 不知道 T 到底是啥,无法实例化 |
不能 instanceof List<String> | 运行时没有 String 这个信息,instanceof 只能判断原始类型 List |
不能创建泛型数组 new T[10] | 数组在运行时记住元素类型,而 T 擦除了,会破坏类型安全 |
为什么叫「擦除」而不是「真泛型」
C++ 模板是「真泛型」(每份类型生成一份机器码,膨胀但快);Java 为了向后兼容(泛型出现前已有一堆 List 字节码),选择在编译期做类型检查、生成时把类型抹掉成 Object+强转。这叫「伪泛型 / 类型擦除」。代价是上面的限制,收益是老代码无缝共存。
四、桥方法(bridge method):擦除如何不破坏多态
擦除会威胁「重写」的正确性。看这个例子:
class Node<T> {
public void setData(T data) { ... }
}
class MyNode extends Node<Integer> {
@Override
public void setData(Integer data) { ... } // 我们想重写
}擦除后,父类 Node 的 setData 变成 setData(Object),而子类是 setData(Integer)——签名不同,按理说不是重写,多态会失效!为保证多态,javac 在子类里自动合成一个桥方法:
// 子类 MyNode 编译后实际包含:
setData(Object data) { // 桥方法:擦除后的「父签名」
this.setData((Integer) data); // 内部转调真正的 setData(Integer)
}
setData(Integer data) { ... } // 你写的方法桥方法让「按 Object 签名调用的父类引用,仍能分派到子类的 Integer 版本」,从而维持了多态。你在 javap 里会看到带 ACC_BRIDGE 标志的奇怪方法,就是它。
五、通配符与 PECS
泛型不通配时,Box<Integer> 不是 Box<Number> 的子类(泛型不变性)。要灵活表达「某类集合」,用通配符:
? extends T(上界):可读取为T,不能写(除null)。生产者,往外给数据。? super T(下界):可写入T及其子类,读取只能当Object。消费者,接收数据。?(无界):? extends Object,只读。
// 上界:可读取为 Number,不能往里写(编译器不知道具体是 Integer 还是 Double)
static double sum(List<? extends Number> list) {
double s = 0;
for (Number n : list) s += n.doubleValue();
return s;
}
// 下界:可写入 Integer 及其子类
static void addInts(List<? super Integer> list) {
list.add(1);
list.add(2);
}PECS 原则:Producer(生产者)用 ? extends T,Consumer(消费者)用 ? super T。Java 标准库就是范例:
// 从 src(生产者) 读,写入 dest(消费者)
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }? extends T 为什么不能写?
因为 List<? extends Number> 可能是 List<Integer> 也可能是 List<Double>。若允许 add(1.5),而实际是 Integer 列表就坏了——编译器为了类型安全,干脆禁止向「上界通配」列表写入非 null 元素。? super T 能写 T 及其子类,因为下界保证列表至少能容纳 T。
六、常见误区与注意点
WARNING
- 泛型不能用于基本类型:
List<int>非法,必须用List<Integer>(自动装箱)。 - 不能
new T[]或创建泛型数组:类型擦除导致运行时无法确定T,用List<T>替代或ArrayList而非T[]。 - 混淆
List<Object>与List<?>:List<Object>只能add(Object);List<?>表示「某种未知类型」,不能add(除 null)。两者不同。 - 误以为运行时能区分泛型:擦除后
List<String>与List<Integer>是同一类型,不能靠instanceof区分具体泛型。 - 裸类型(raw type):
List list = new ArrayList()绕过泛型检查,编译器只给警告,埋下ClassCastException隐患,禁止在生产用。
七、最佳实践
- 集合一律使用泛型,杜绝裸类型
List。2. 方法能泛型化就泛型化,减少重复代码与强转。3. 读多写少用? extends,写多读少用? super(PECS)。4. 不要用泛型数组、不要在泛型里做依赖具体类型的instanceof。5. 自定义泛型类时,T 的「上限」用extends约束(如<T extends Comparable<T>>),让 T 能调用对应方法。
📚 官方文档与参考资料
- Java SE 21 文档:https://docs.oracle.com/en/java/javase/21/
- Java 语言规范 · 类型擦除:https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.6
- Java 教程 · 泛型:https://docs.oracle.com/javase/tutorial/java/generics/
- OpenJDK 项目:https://openjdk.org/
✏️ 练习与自测
- 写一个泛型类
Pair<K, V>,保存一对键值并分别提供getKey()/getValue();用javap -c或javap -p观察擦除后方法返回类型变成了什么。 - 写泛型方法
max(T[] arr, Comparator<T> c)返回数组最大值(提示:<T extends Comparable<? super T>>或传Comparator)。 - 用 PECS 解释
Collections.copy(List<? super T> dest, List<? extends T> src):为什么dest用super、src用extends?尝试传一个List<Number> dest和List<Integer> src验证类型合法。 - 解释为什么
List<Integer> li = new ArrayList<Integer>();与List<String>在运行时是「同一个类」?(结合类型擦除)
本节小结
泛型在编译期保证类型安全、消除强转。关键是类型擦除:运行时 T 被替换为上限(Object),Box<String> 与 Box<Integer> 是同一类;这导致「不能用基本类型、不能 new T[]、instanceof 只能判原始类型、无泛型数组」。为维持重写多态,编译器合成桥方法(擦除签名 → 内部转调真实方法)。泛型不变性靠通配符打破:? extends T 只读(生产者)、? super T 可写(消费者),PECS 原则指导签名设计。用泛型、禁裸类型,享受编译期保护。