第12章:泛型
本章语言侧重:Java ⭐⭐⭐ | Python 和 C++ 的对比在末尾给出。
📌 代码说明:标注"可复制运行"的代码块包含完整的
main方法。没标注的是概念示意片段。
一个故事开头
你的奶茶店进了一批新货:杯子、原料、优惠券。每种东西都要登记入库。
你写了一个"通用盒子"类——什么都能装:
// 一个什么都能装的盒子
public class Box {
private Object item; // Object 是所有类的祖宗
public void set(Object item) { this.item = item; }
public Object get() { return item; }
}看起来很方便——杯子也能装,珍珠也能装,优惠券也能装。
Box cupBox = new Box();
cupBox.set("陶瓷杯"); // 装的杯子
Box pearlBox = new Box();
pearlBox.set(100); // 装的珍珠数量
// 取出来的时候...
String cup = (String) cupBox.get(); // 要强制转
int pearls = (int) pearlBox.get(); // 也要强制转每次都要强制转换,烦。而且更危险的是——如果你搞混了类型:
cupBox.set(42); // 杯子里放了数字 42,逻辑上不对,但编译通过了
String cup = (String) cupBox.get(); // ❌ 运行时报 ClassCastException!编译时没人告诉你错了,跑起来才炸。
泛型(Generics)就是解决这个问题的——它让你写代码的时候就说清楚"这个盒子只能装什么类型",编译器帮你检查。
1. 泛型类——给类加个"类型参数"
// 可复制运行,保存为 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. 泛型方法——方法级别的类型参数
不只是类可以有类型参数,方法也可以:
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. 多个类型参数
// 可复制运行,保存为 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 的泛型是编译期的概念。 编译成字节码之后,类型参数的信息就被擦除了。
// 你写的代码(有泛型)
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();
// 编译后(类型被擦除)
Box stringBox = new Box(); // 变成了原始的 Box
Box intBox = new Box();这意味着什么?
Box<String> sBox = new Box<>();
Box<Integer> iBox = new Box<>();
System.out.println(sBox.getClass() == iBox.getClass()); // true!
// 运行时它们都是 Box,没有 String 和 Integer 的区别💥 拆了它:验证类型擦除
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>——它们全是泛型。
List<String> names = new ArrayList<>(); // ArrayList<E> 是泛型类
Map<String, Integer> scores = new HashMap<>(); // HashMap<K,V> 也是第10章没讲泛型,你照样用了。 这就是泛型的设计目标——对日常使用者来说,它"自动工作"。
6. 类型边界——限定 T 能是什么
有时候你想说"T 必须是某个类的子类":
// 限定 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. 通配符——? 表示"未知类型"
// 这个方法想接受任何类型的 Box
public static void printBox(Box<?> box) { // ? = 任意类型
// box.set("hello"); // ❌ 不能放!因为不知道 ? 是什么类型
System.out.println(box.get()); // ✅ 可以取,取出来是 Object
}上界通配符 ? extends T
只能读,不能写。告诉 Java"列表里的元素是 T 或 T 的子类"。
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)。
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
// 从 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. 完整例子:奶茶店库存管理系统
// 可复制运行,保存为 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. 三语言对照
| 特性 | Java | Python | C++ |
|---|---|---|---|
| 泛型机制 | 类型擦除(编译期) | 动态类型(不需要泛型) | 模板(编译期实例化) |
| 类型安全 | 编译期检查 | 运行时检查 | 编译期检查 |
| 类型参数约束 | extends 边界 | 无(鸭子类型) | concept(C++20) |
| 通配符 | ? extends / ? super | 不需要 | 不需要(用特化) |
| 性能 | 无运行时开销(擦除) | 运行时类型检查 | 每个实例化生成独立代码 |
# 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// 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;
}本章小结
- 泛型 = 类型参数——写类或方法时说"以后你再告诉我具体是什么类型"
- 类型安全——编译期检查,不用强制转换,不会 ClassCastException
- 类型擦除——泛型只在编译期存在,运行时不存在(和 C++ 模板不同)
<T extends Number>——限定类型参数必须是某个类的子类? extends/? super——通配符,PECS 原则- 日常更多是"用"泛型——
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 方法)