Skip to content

Chapter 15 · 泛型(Generics)

一份代码,处理多种类型 — 既不丢类型安全,又不丢通用性。


🎯 本章目标

  • 理解泛型解决了什么问题
  • 掌握 泛型类、泛型方法、泛型接口 的写法
  • 学会 通配符 ?extends 上界 vs super 下界
  • 知道 类型擦除(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);

💡 类型参数命名约定

  • T Type
  • E Element(集合元素)
  • K, V Key, Value
  • R Return
  • N Number

泛型类型参数


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 warning

8.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 泛型是类型擦除("假泛型")

🔗 导航