Skip to content

第12章:泛型

本章语言侧重:Java ⭐⭐⭐ | Python 和 C++ 的对比在末尾给出。

📌 代码说明:标注"可复制运行"的代码块包含完整的 main 方法。没标注的是概念示意片段。

一个故事开头

你的奶茶店进了一批新货:杯子、原料、优惠券。每种东西都要登记入库。

你写了一个"通用盒子"类——什么都能装:

java
// 一个什么都能装的盒子
public class Box {
    private Object item;  // Object 是所有类的祖宗
    
    public void set(Object item) { this.item = item; }
    public Object get() { return item; }
}

看起来很方便——杯子也能装,珍珠也能装,优惠券也能装。

java
Box cupBox = new Box();
cupBox.set("陶瓷杯");          // 装的杯子

Box pearlBox = new Box();
pearlBox.set(100);             // 装的珍珠数量

// 取出来的时候...
String cup = (String) cupBox.get();      // 要强制转
int pearls = (int) pearlBox.get();       // 也要强制转

每次都要强制转换,烦。而且更危险的是——如果你搞混了类型:

java
cupBox.set(42);  // 杯子里放了数字 42,逻辑上不对,但编译通过了
String cup = (String) cupBox.get();  // ❌ 运行时报 ClassCastException!

编译时没人告诉你错了,跑起来才炸。

泛型(Generics)就是解决这个问题的——它让你写代码的时候就说清楚"这个盒子只能装什么类型",编译器帮你检查。


1. 泛型类——给类加个"类型参数"

java
// 可复制运行,保存为 Box.java
public class Box<T> {       // T 是类型参数——"你告诉我这个盒子装什么"
    private T item;         // item 的类型就是 T
    
    public void set(T item) {
        this.item = item;
    }
    
    public T get() {
        return item;
    }
    
    public static void main(String[] args) {
        // 创建只装 String 的盒子
        Box<String> stringBox = new Box<>();
        stringBox.set("陶瓷杯");   // ✅ 只能放 String
        // stringBox.set(42);      // ❌ 编译错误!42 不是 String
        String cup = stringBox.get();  // ✅ 不用强制转换
        
        // 创建只装 Integer 的盒子
        Box<Integer> intBox = new Box<>();
        intBox.set(100);
        int pearls = intBox.get();     // ✅ 自动拆箱,不用强制转换
    }
}

对比一下:

没用泛型(Object)用了泛型
存放什么都能放只能放指定的类型
取出要强制转换 (String)直接取,类型已知
类型检查运行时才报错编译时就报错

2. 泛型方法——方法级别的类型参数

不只是类可以有类型参数,方法也可以:

java
public class Utils {
    // 泛型方法——<T> 写在返回值前面
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

// 使用
String[] drinks = {"波波奶茶", "杨枝甘露", "柠檬茶"};
Integer[] counts = {10, 20, 15};

Utils.printArray(drinks);  // 波波奶茶 杨枝甘露 柠檬茶
Utils.printArray(counts);  // 10 20 15

同一个方法,处理不同类型的数组——不用写重载。


3. 多个类型参数

java
// 可复制运行,保存为 Pair.java
public class Pair<K, V> {     // K = key 的类型, V = value 的类型
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
    
    public static void main(String[] args) {
        Pair<String, Integer> order = new Pair<>("波波奶茶", 3);
        System.out.println(order.getKey() + " × " + order.getValue());
    }
}

HashMap 其实就是用了两个类型参数——HashMap<K, V>


4. 类型擦除——泛型只在编译期存在

这是泛型最重要也最容易困惑的特性。

Java 的泛型是编译期的概念。 编译成字节码之后,类型参数的信息就被擦除了。

java
// 你写的代码(有泛型)
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();

// 编译后(类型被擦除)
Box stringBox = new Box();     // 变成了原始的 Box
Box intBox = new Box();

这意味着什么?

java
Box<String> sBox = new Box<>();
Box<Integer> iBox = new Box<>();

System.out.println(sBox.getClass() == iBox.getClass());  // true!
// 运行时它们都是 Box,没有 String 和 Integer 的区别

💥 拆了它:验证类型擦除

java
import java.util.*;

public class ErasureDemo {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> ints = new ArrayList<>();
        
        System.out.println(strings.getClass() == ints.getClass());  // true
        
        // 虽然编译时 List<String> 不能放 Integer
        // strings.add(42);  // ❌ 编译错误
        
        // 但通过反射可以在运行时绕过泛型检查
        try {
            strings.getClass().getMethod("add", Object.class).invoke(strings, 42);
            System.out.println("通过反射放入了 Integer: " + strings.get(0));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

实际影响

  • 你不能写 new T()(不知道 T 是啥,没法 new)
  • 你不能写 new T[10](不能创建泛型数组)
  • instanceof 不能跟泛型类型一起用(if (obj instanceof Box<String>) 编译错误)

5. 泛型和集合——你已经在用了

第10章你写的 ArrayList<String>HashMap<String, Integer>——它们全是泛型。

java
List<String> names = new ArrayList<>();   // ArrayList<E> 是泛型类
Map<String, Integer> scores = new HashMap<>();  // HashMap<K,V> 也是

第10章没讲泛型,你照样用了。 这就是泛型的设计目标——对日常使用者来说,它"自动工作"。


6. 类型边界——限定 T 能是什么

有时候你想说"T 必须是某个类的子类":

java
// 限定 T 必须是 Number 或其子类
public class Calculator<T extends Number> {
    private T value;
    
    public Calculator(T value) {
        this.value = value;
    }
    
    public double doubleValue() {
        return value.doubleValue();  // Number 类有 doubleValue() 方法
    }
}

// 只能传 Number 的子类(Integer, Double 等)
Calculator<Integer> calc1 = new Calculator<>(100);
Calculator<Double> calc2 = new Calculator<>(3.14);
// Calculator<String> calc3 = ...  // ❌ 编译错误!String 不是 Number 的子类

<T extends Number> = "T 是 Number 或它的子类"。


7. 通配符——? 表示"未知类型"

java
// 这个方法想接受任何类型的 Box
public static void printBox(Box<?> box) {   // ?  = 任意类型
    // box.set("hello");  // ❌ 不能放!因为不知道 ? 是什么类型
    System.out.println(box.get());  // ✅ 可以取,取出来是 Object
}

上界通配符 ? extends T

只能读,不能写。告诉 Java"列表里的元素是 T 或 T 的子类"。

java
public static double sumOfList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) {
        sum += n.doubleValue();
    }
    return sum;
}

List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
System.out.println(sumOfList(ints));     // 6.0
System.out.println(sumOfList(doubles));  // 7.5

下界通配符 ? super T

只能写,不能读(读出来是 Object)。

java
public static void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

List<Number> numbers = new ArrayList<>();
addNumbers(numbers);  // ✅ Number 是 Integer 的父类

PECS 原则——Producer Extends, Consumer Super

这个缩写有点难记,但很有用:

  • 如果参数是"生产者"(你要从里面读数据)→ ? extends T
  • 如果参数是"消费者"(你要往里面写数据)→ ? super T
java
// 从 list 里读(生产者)→ extends
void readOnly(List<? extends Number> list) {
    Number n = list.get(0);  // ✅ 读,安全
}

// 往 list 里写(消费者)→ super
void writeOnly(List<? super Integer> list) {
    list.add(42);  // ✅ 写,安全
}

实际项目中你不会天天写泛型——你更多的是用现成的泛型类(List、Map)和调用泛型方法。但理解这些能让你在需要时自己写出类型安全的代码。


8. 完整例子:奶茶店库存管理系统

java
// 可复制运行,保存为 InventoryDemo.java
import java.util.*;

// 泛型库存类
class Inventory<T> {
    private List<T> items = new ArrayList<>();
    private int capacity;
    
    public Inventory(int capacity) {
        this.capacity = capacity;
    }
    
    public void add(T item) {
        if (items.size() >= capacity) {
            throw new RuntimeException("库存已满");
        }
        items.add(item);
    }
    
    public T remove(int index) {
        return items.remove(index);
    }
    
    public int size() {
        return items.size();
    }
    
    public List<T> getAll() {
        return new ArrayList<>(items);  // 返回副本,保护内部数据
    }
}

// 库存物品
record Ingredient(String name, int quantity) {}
record Cup(String style, int size) {}

public class InventoryDemo {
    // 统计任意类型库存的总数
    public static <T> void printInventory(Inventory<T> inv, String label) {
        System.out.println(label + " 库存: " + inv.size() + "件");
    }
    
    public static void main(String[] args) {
        // 原料库存
        Inventory<Ingredient> ingredientInv = new Inventory<>(50);
        ingredientInv.add(new Ingredient("珍珠", 100));
        ingredientInv.add(new Ingredient("茶底", 30));
        ingredientInv.add(new Ingredient("牛奶", 20));
        printInventory(ingredientInv, "原料");
        
        // 杯子库存
        Inventory<Cup> cupInv = new Inventory<>(100);
        cupInv.add(new Cup("中杯", 360));
        cupInv.add(new Cup("大杯", 500));
        printInventory(cupInv, "杯子");
        
        // 遍历原料
        System.out.println("\n原料清单:");
        for (Ingredient ing : ingredientInv.getAll()) {
            System.out.println("  " + ing.name() + " × " + ing.quantity());
        }
    }
}

9. 三语言对照

特性JavaPythonC++
泛型机制类型擦除(编译期)动态类型(不需要泛型)模板(编译期实例化)
类型安全编译期检查运行时检查编译期检查
类型参数约束extends 边界无(鸭子类型)concept(C++20)
通配符? extends / ? super不需要不需要(用特化)
性能无运行时开销(擦除)运行时类型检查每个实例化生成独立代码
python
# Python 不需要泛型——类型是动态的
class Box:
    def __init__(self, item):
        self.item = item
    
    def get(self):
        return self.item

box = Box("陶瓷杯")
print(box.get())       # 陶瓷杯
box.item = 42          # 可以——Python 不限制
print(box.get())       # 42
cpp
// C++ 模板——每个 T 生成独立的代码
#include <iostream>
using namespace std;

template <typename T>
class Box {
private:
    T item;
public:
    void set(T i) { item = i; }
    T get() { return item; }
};

int main() {
    Box<string> box;
    box.set("陶瓷杯");
    cout << box.get() << endl;
    return 0;
}

本章小结

  1. 泛型 = 类型参数——写类或方法时说"以后你再告诉我具体是什么类型"
  2. 类型安全——编译期检查,不用强制转换,不会 ClassCastException
  3. 类型擦除——泛型只在编译期存在,运行时不存在(和 C++ 模板不同)
  4. <T extends Number> ——限定类型参数必须是某个类的子类
  5. ? extends / ? super ——通配符,PECS 原则
  6. 日常更多是"用"泛型——List<String>Map<K,V> 天天用,自己写泛型较少

✅ 验收标准

完成本章后,你应该能:

  • [ ] 定义并使用泛型类(如 Box<T>
  • [ ] 定义并使用泛型方法
  • [ ] 用 extends 限定类型参数边界
  • [ ] 使用通配符 ? extends? super
  • [ ] 理解类型擦除及其影响

📌 常见卡点

  • 不能创建泛型数组 new T[10]
  • 静态成员不共享类类型参数
  • 泛型类型的 instanceof 检查行不通
  • 基本类型不能作类型参数——用包装类

🔜 现在不需要理解

  • 泛型在底层 JVM 的实现细节
  • 桥方法(bridge method)——编译器的把戏
  • Capture of ? 通配符捕获——极少需要自己写


🧪 练习

1. 泛型类:定义一个 Triple<A, B, C> 类,能存三个不同类型的值,并提供 getter。

2. 泛型方法:写一个 static <T> T getMiddle(T[] array) 方法,返回数组中间的元素。

3. 边界:写一个方法 static <T extends Comparable<T>> T max(T a, T b),返回较大的那个。(提示:Comparable 接口有 compareTo 方法)


下一篇

第13章 注解、反射与元编程

用 ❤️ 构建 | Software Systems Atlas