主题
Chapter 15 · 泛型(Generics)
一份代码,处理多种类型 — 既不丢类型安全,又不丢通用性。
🎯 本章目标
- 理解泛型解决了什么问题
- 掌握 泛型类、泛型方法、泛型接口 的写法
- 学会 通配符 ?:
extends上界 vssuper下界 - 知道 类型擦除(Type Erasure)的真相
- 用对 PECS 原则
1. 没有泛型会怎样?
JDK 5 之前,集合存的是 Object:
java
List list = new ArrayList(); // 没有类型
list.add("hello");
list.add(42); // 啥都能往里塞
list.add(new Date());
String s = (String) list.get(1); // ClassCastException!问题:
- 类型不安全:编译期不报错,运行时崩
- 强转麻烦:每次取出都要 cast
JDK 5 引入泛型:编译期类型检查。
java
List<String> list = new ArrayList<>();
list.add("hello");
list.add(42); // ❌ 编译错误!
String s = list.get(0); // 不用强转2. 泛型类
java
public class Box<T> { // T 是类型参数
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Box<String> b1 = new Box<>("hello");
Box<Integer> b2 = new Box<>(42);
String s = b1.get(); // 不用强转多个类型参数
java
public class Pair<K, V> {
K key; V value;
public Pair(K k, V v) { this.key = k; this.value = v; }
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<String, Integer> p = new Pair<>("age", 25);💡 类型参数命名约定:
TTypeEElement(集合元素)K, VKey, ValueRReturnNNumber
3. 泛型方法
java
public class Util {
// <T> 在返回类型前
public static <T> T pickFirst(List<T> list) {
return list.get(0);
}
public static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
public static <K, V> Map<V, K> reverse(Map<K, V> map) {
Map<V, K> r = new HashMap<>();
map.forEach((k, v) -> r.put(v, k));
return r;
}
}
String first = Util.pickFirst(List.of("a", "b", "c"));💡 泛型方法的类型参数 声明在返回类型之前!
4. 泛型接口
java
public interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
void deleteById(ID id);
}
class UserRepository implements Repository<User, Long> {
@Override public User findById(Long id) { ... }
@Override public void save(User user) { ... }
@Override public void deleteById(Long id){ ... }
}5. 类型擦除(重要!)
🔑 Java 泛型是"假泛型",运行时类型信息会被擦除。
java
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true ❗为什么?为了和 JDK 5 之前的代码兼容(向后兼容),编译后泛型信息全部擦除:
List<String>→List<T>→Object(无界)或上界类型
后果:
- 不能
new T() - 不能
T[] arr = new T[10] - 不能
obj instanceof T - 不能用泛型作为
static字段类型
6. 通配符 ? 与 PECS 原则
上界通配符 <? extends T>
java
public static double sum(List<? extends Number> list) {
double s = 0;
for (Number n : list) s += n.doubleValue();
return s;
}
sum(List.of(1, 2, 3)); // List<Integer>
sum(List.of(1.0, 2.0)); // List<Double>💡
<? extends Number>= "可以是 Number 或它的任何子类"
限制:只能 读,不能 写!
java
List<? extends Number> list = ...;
Number n = list.get(0); // ✓ 可以读
list.add(1); // ❌ 不能写下界通配符 <? super T>
java
public static <T> void copy(List<? super T> dst, List<T> src) {
for (T t : src) dst.add(t);
}💡
<? super Integer>= "可以是 Integer 或它的任何父类"
限制:只能 写,读出来都是 Object。
PECS 原则
Producer Extends, Consumer Super
- 这个集合是 生产者(被你读)→ 用
extends - 这个集合是 消费者(被你写)→ 用
super
java
public static <T> void copy(
List<? super T> dst, // 接收方(消费者)
List<? extends T> src // 提供方(生产者)
) { ... }7. 边界类型参数
java
// 限定 T 必须是 Number 或它的子类
public static <T extends Number> double sumAll(List<T> list) {
double s = 0;
for (T n : list) s += n.doubleValue(); // 可以调 Number 的方法
return s;
}
// 多个上界(只能有一个是类,其他必须是接口)
public static <T extends Number & Comparable<T>> T max(List<T> list) {
T m = list.get(0);
for (T t : list) if (t.compareTo(m) > 0) m = t;
return m;
}8. 常见坑
8.1 不能 new 泛型
java
class Box<T> {
T value = new T(); // ❌ 编译错误
T[] arr = new T[10]; // ❌ 编译错误
}解决:传 Class 对象或工厂:
java
class Box<T> {
T value;
Box(Class<T> clazz) throws Exception {
this.value = clazz.getDeclaredConstructor().newInstance();
}
}8.2 数组不能放泛型
java
List<String>[] lists = new List<String>[10]; // ❌
List<String>[] lists = new List[10]; // ⚠️ unchecked warning8.3 静态字段不能用类的泛型
java
class Box<T> {
static T staticField; // ❌ static 字段不能用类的 T
static <T> T method() {} // ✓ 静态方法可以有自己的泛型
}9. 实战练习
| 文件 | 内容 |
|---|---|
Box.java | 基础泛型类 |
Pair.java | 双类型参数 |
GenericMethodDemo.java | 泛型方法演示 |
WildcardDemo.java | 通配符与 PECS |
SimpleStack.java | 实战:泛型栈 |
10. 浏览器演示
打开 demo.html:
- 类型擦除可视化
- PECS 原则交互
- 通配符使用场景
11. 面试可能会问什么?
Q1: Java 泛型是怎么实现的?
类型擦除。编译期检查类型,编译后所有 <T> 都变成 Object(或上界类型),class 文件里没有泛型信息。
Q2: List<Object> 和 List<?> 区别?
List<Object>:精确,只能传List<Object>List<?>:通配,可以传List<任何类型>,但不能写入(除 null)
Q3: 为什么不能 new T()?
类型擦除:运行时不知道 T 是什么。编译器不让你写。
Q4: PECS 原则是什么?
- Producer(被读)→ extends
- Consumer(被写)→ super
java
Collections.copy(List<? super T> dst, List<? extends T> src);Q5: 数组协变 vs 泛型不协变
java
Object[] objs = new String[10]; // ✓ 数组协变
objs[0] = 1; // ✗ 运行时 ArrayStoreException
List<Object> list = new ArrayList<String>(); // ❌ 编译错误(泛型不协变)泛型设计成不协变是为了 编译期类型安全,避免数组那种运行时错误。
Q6: 桥接方法(Bridge Method)是什么?
由于类型擦除,子类重写父类的泛型方法时,编译器会自动生成一个"桥接方法",保证多态正确。
🎁 本章小结
✅ 泛型:编译期类型检查,避免运行时崩
✅ 泛型类、泛型方法、泛型接口
✅ <? extends T> 上界(生产者,只读)
✅ <? super T> 下界(消费者,只写)
✅ PECS:Producer Extends, Consumer Super
✅ Java 泛型是类型擦除("假泛型")🔗 导航
- ⬅️ 上一章:Chapter 14 · 集合框架
- ➡️ 下一章:Chapter 16 · IO 流与文件操作