泛型基础
约 1183 字大约 4 分钟
布欧-Lewyon
2026-05-15
泛型解决的问题很直接:让代码在多种类型上复用,同时保留编译期类型检查。没有泛型之前,程序员只能写 Object 类型参数并在每次取值时手动强制转换——转换错了类型,运行时才崩溃。
泛型类
在类名后加 <T> 即可引入类型参数,类体内可将其用作字段、方法参数和返回值的类型。实例化时指定具体类型参数:
public class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}Box<String> stringBox = new Box<>();
stringBox.set("hello");
String val = stringBox.get(); // 无需强制转型
Box<Integer> intBox = new Box<>();
intBox.set(42);
// intBox.set("hello"); // 编译报错,类型已锁定为 Integer- 菱形运算符
<>(Java 7+)让编译器根据左侧声明推断实际类型,右侧不必重复写<String>。 - 类型参数只能用引用类型,不能是基本类型(如
Box<int>编译不通过,需用Box<Integer>)。
泛型方法
类型参数也可只作用在单个方法上,与类级别的泛型无关:
public class Utils {
public static <T> T getMiddle(T... args) {
return args[args.length / 2];
}
}String mid = Utils.getMiddle("a", "b", "c"); // "b"
Integer mid2 = Utils.getMiddle(1, 2, 3, 4, 5); // 3类型参数 <T> 写在返回类型之前;调用时通常可省略显式类型声明,编译器通过实参推断。
类型边界
用 extends 限定类型参数必须是某个类型或其子类型。<T extends Number> 表示 T 只能是 Number 及其子类(如 Integer、Double),因此可以安全地调用 Number 的方法:
public class Stats<T extends Number> {
private T[] numbers;
public Stats(T[] numbers) {
this.numbers = numbers;
}
public double average() {
double sum = 0;
for (T n : numbers) {
sum += n.doubleValue(); // T 保证了 doubleValue() 可用
}
return sum / numbers.length;
}
}通配符 ?
通配符解决参数化类型之间的继承关系问题。Box<Number> 不是 Box<Integer> 的父类——即使 Integer 是 Number 的子类。通配符让方法可以接受类型参数的某个范围。
上界通配符 ? extends T
表示类型参数是 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 = List.of(1, 2, 3);
sumOfList(ints); // ✅ List<Integer> 可传入 List<? extends Number>但从 ? extends Number 的集合中写入是不安全的——编译器不知道具体类型,禁止调用带类型参数的 add 方法(可添加 null)。
下界通配符 ? super T
表示类型参数是 T 或 T 的父类,适合写入(消费者):
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i); // ✅ Integer 可添加到 List<? super Integer>
}
}List<Number> nums = new ArrayList<>();
addNumbers(nums); // ✅ List<Number> 可传入 List<? super Integer>PECS 原则
PECS:Producer extends,Consumer super。
- 方法从参数中读取对象 → 参数用
? extends T(生产者) - 方法向参数中写入对象 → 参数用
? super T(消费者) - 既读又写 → 不用通配符,直接用
T
类型擦除
泛型是编译期特性。编译后的字节码中,类型参数被替换为边界类型(若未指定边界则为 Object),并插入必要的强制转换。这个过程称为类型擦除(type erasure)。
// 源码
Box<String> box = new Box<>();
box.set("hello");
String val = box.get();
// 编译后(擦除后大致等价于)
Box box = new Box();
box.set("hello");
String val = (String) box.get();擦除带来的限制:
| 限制 | 为什么 | 替代方案 |
|---|---|---|
new T() | 运行时不知道 T 是什么 | 传入 Class<T> 反射创建 |
instanceof T | 运行时类型参数已擦除 | instanceof 检查边界类型 |
new T[n] | 无法确定数组具体类型 | 用 ArrayList<T> 替代 |
静态字段 static T field | 静态字段属于类,所有泛型变体共享 | 不可用 |
小结
- 泛型在类(
class Box<T>)或方法(<T> void method(T arg))上引入类型参数,提供编译期类型检查,消除强制转换。 - 类型边界
<T extends Number>限制类型参数的范围,并允许调用边界类型的方法。 - 通配符
? extends T(生产者/读取)与? super T(消费者/写入)解决泛型类型的协变/逆变问题,遵循 PECS 原则。 - 类型擦除意味着泛型只在编译期有效,运行时类型参数已消失。
- 易错:泛型不可用于基本类型(用包装类替代);不能创建泛型数组
new T[n];List<String>和List<Integer>在运行时是同一个 Class 对象。 - 思考任务:写一个静态方法
copy(List<? extends T> src, List<? super T> dest),将 src 全部拷贝到 dest,体会 PECS 原则。
上一节:异常
下一节:List 与 Set
