目录
📌 PECS 原则(Producer Extends, Consumer Super)
一、引言:
作为一个即将毕业的 Java 新人,在刷面经的时候我发现,“泛型”几乎是每个大厂面试都会问到的知识点。刚开始我以为它只是个语法糖,写代码时方便而已。
但随着学习深入,我才意识到:泛型不仅是集合安全的保障,更是写出高质量代码的关键工具之一。
这篇文章就记录了我从“看不太懂”到“基本理解”的过程。如果你也觉得泛型难懂,或者在面试中被问得哑口无言,那这篇内容应该能帮你打通任督二脉!
二、什么是泛型?我能不用吗?
先说结论:你当然可以不用泛型,但代价是代码更容易出错,而且维护成本更高。
✅ 示例1:用了泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 不需要强转
❌ 没用泛型(Java 5 之前)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 看似没问题
String str = (String) list.get(1); // 运行时报错 ClassCastException
📌 面试常问:
“泛型的作用是什么?”
- 提高类型安全性;
- 减少强制类型转换;
- 增强代码可读性。
三、泛型类 & 泛型方法:怎么写才对?
泛型最常见的两种应用方式是泛型类和泛型方法。
✅ 示例2:泛型类
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
使用:
Box<String> stringBox = new Box<>();
stringBox.set("Java");
System.out.println(stringBox.get());
✅ 示例3:泛型方法
public class Util {
public static <T> void printList(List<T> list) {
for (T item : list) {
System.out.println(item);
}
}
}
调用:
List<Integer> numbers = List.of(1, 2, 3);
Util.printList(numbers);
📌 小贴士:
- 如果整个类都需要参数化类型,选泛型类;
- 如果只是某个方法需要用到泛型,用泛型方法即可;
- 两者都可以结合使用。
四、通配符与 PECS 原则:别再被绕晕了!
这个部分可能是很多同学最头疼的地方:为什么有时候不能 add 元素?为什么返回值变成了 Object?
其实这都跟通配符有关。
🤔 我遇到的问题:
有一次我在遍历一个 List<? extends Animal>
,想往里面加一只 Dog
,结果编译器报错了。我当时一脸懵:不是说 Dog 是 Animal 的子类吗?为什么会不让加?
后来我才明白:虽然 Dog 是 Animal 的子类,但编译器不知道这个 List 到底装的是哪种 Animal 的子类。
✅ 示例4:上界通配符 <? extends T>
List<? extends Number> list = new ArrayList<Integer>();
// list.add(100); ❌ 编译错误
Number num = list.get(0); // ✅ 可以读取
📌 总结一句话:
extends
是只读的,只能读不能写。
✅ 示例5:下界通配符 <? super T>
List<? super Integer> list = new ArrayList<Number>();
list.add(100); // ✅ 可以添加 Integer 及其子类对象
Object obj = list.get(0); // ⚠️ 返回值是 Object,无法确定具体类型
📌 总结一句话:
super
是只写的,只能添加当前类型及子类,但读出来是 Object。
📌 PECS 原则(Producer Extends, Consumer Super)
这是《Effective Java》里提到的一个原则,用来判断什么时候该用哪种通配符:
场景 | 用法 |
---|---|
数据生产者(读取) | ? extends T |
数据消费者(写入) | ? super T |
📌 举个例子:
public static void copy(List<? super String> dest, List<? extends String> src) {
for (String s : src) {
dest.add(s);
}
}
📌 面试高频题:
“请解释
<? extends T>
和<? super T>
的区别?”
extends
:只能读不能写;super
:只能写不能读;- PECS 原则是选择依据。
五、类型擦除:泛型真的存在于运行时吗?
这个问题是我最困惑的之一。我以为泛型是运行时存在的,结果发现它其实是个“假象”。
✅ 示例6:验证类型擦除
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // 输出 true
📌 说明:
- 在运行时,这两个 List 的类型信息都被“擦掉了”,变成原始类型
ArrayList
。 - 所以 JVM 根本不知道你放进去的是 String 还是 Integer。
❌ 常见误区:为什么不能 new T()?
public class Box<T> {
T[] array = new T[10]; // ❌ 编译错误
}
📌 原因:
- 因为泛型在运行时不存在,所以 JVM 不知道
T
是什么类型,也就没法创建数组或实例。
📌 解决办法:
- 用反射传入
Class<T>
; - 或者通过传递数组实例。
📌 类型擦除的影响总结:
问题描述 | 原因 |
---|---|
不能 new T() 或 T[] | 类型擦除导致 JVM 不知道 T 是什么类型 |
不能获取泛型类型信息 | 运行时泛型信息已被擦除 |
方法重载冲突 | 类型擦除后两个方法签名相同,编译失败 |
桥方法 | 编译器自动生成桥方法保持多态性 |
六、泛型在集合中的作用:为什么集合都用泛型?
Java 集合框架从一开始设计就考虑到了泛型的支持。比如:
List<E>
Set<E>
Map<K,V>
它们之所以广泛使用泛型,是因为:
- 提升类型安全性:避免运行时 ClassCastException;
- 增强可读性:一看就知道集合里放的是什么类型;
- 简化开发:不需要频繁做类型转换。
七、实战避坑指南:泛型常见误区
问题描述 | 正确做法 |
---|---|
试图 new T() 或 new T[] | 用反射或传递 Class 对象解决 |
自定义类放入 HashSet 未重写 hashCode/equals | 导致去重失败 |
使用普通 for 循环删除集合元素 | 抛出 ConcurrentModificationException |
误用 <? extends T> 进行写入操作 | 无法 add 元素 |
误用 <? super T> 进行读取操作 | 返回值为 Object 类型 |
八、总结:泛型知识点速记表
核心知识点 | 内容说明 |
---|---|
泛型的作用 | 提高类型安全性、减少强制类型转换 |
泛型形式 | 泛型类、泛型接口、泛型方法 |
通配符 | ? extends T (只读)、? super T (只写) |
PECS 原则 | Producer Extends, Consumer Super |
类型擦除 | 编译时泛型信息被擦除,运行时不可见 |
桥方法 | 保证泛型多态性的机制 |
泛型在集合中的作用 | 提升集合的类型安全和代码可读性 |