泛型与类型安全
学习目标
- 定义泛型类、接口和泛型方法,减少强制类型转换。
- 用 extends、super 和 PECS 表达生产者与消费者边界。
- 理解类型擦除、原始类型、通配符和泛型数组限制。
核心知识点
专业术语
- 类型参数:如 T、K、V 的类型变量,在编译期约束输入和输出。
- 通配符:? 表示未知类型,? extends T 为上界,? super T 为下界。
- PECS:Producer Extends、Consumer Super,来源用 extends,写入目标用 super。
- 类型擦除:泛型检查主要在编译期完成,运行时通常只保留原始边界。
- 堆污染:参数化类型引用指向不符合声明类型对象的状态。
白话解释与边界
List<Integer> 不是 List<Number> 的子类型,因为允许替换会把 Double 放入整数列表。List<? extends Number> 可以安全读成 Number,但不能添加任意数字;List<? super Integer> 可以添加 Integer,但读出只能当 Object。泛型不是运行时标签,不能直接写 instanceof List<String>。
详细集合 API 已拆成可检索页面:List 常用 API、Set 去重与集合运算、Queue 与 Deque、Map 常用 API、集合排序与不可变集合。
常用用法
泛型类:让字段和方法保持同一类型
类型参数由调用方具体化,编译器会检查写入和读取,不需要手动强转。
// 语义:类型参数由调用方具体化,编译器会检查写入和读取,不需要手动强转。
final class Box<T> {
private final T value;
Box(T value) {
this.value = value;
}
T value() {
return value;
}
}
public class GenericClassDemo {
public static void main(String[] args) {
Box<Integer> box = new Box<>(42);
System.out.println(box.value());
// 输出:42
}
}泛型方法:让参数和返回值建立关系
方法级类型参数写在返回类型前;编译器通常可根据实参推断 T。
// 语义:方法级类型参数写在返回类型前。
public class GenericMethodDemo {
static <T> T first(T left, T right) {
return left;
}
public static void main(String[] args) {
String value = first("java", "api");
System.out.println(value);
// 输出:java
}
}extends:从生产者读取
extends 允许 List<Integer>、List<Double> 等不同具体列表作为来源;方法只读,不向 source 添加数字。
// 语义:extends 允许 List<。
import java.util.List;
public class ExtendsDemo {
static double sum(List<? extends Number> values) {
double total = 0;
for (Number value : values) {
total += value.doubleValue();
// 输入:total += value.doubleValue();;total 的更新表达式为 value.doubleValue(),按 += 写回。
// 关键变化:total += value.doubleValue();;读取 value.doubleValue() 的结果并累加到 total,total 状态更新。
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
// 输出:6.0
}
}super:向消费者写入
super 能安全接收 Integer 写入 List<Integer>、List<Number> 或 List<Object>;读取结果只能按 Object 使用。
// 语义:super 能安全接收 Integer 写入 List<。
import java.util.ArrayList;
import java.util.List;
public class SuperDemo {
static void addNumbers(List<? super Integer> target) {
target.add(1);
target.add(2);
}
public static void main(String[] args) {
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println(numbers);
// 输出:[1, 2]
}
}PECS:复制来源到目标
来源用 extends,目标用 super;这是 Collections.copy、addAll 等集合 API 背后的常见设计。
// 语义:来源用 extends,目标用 super。
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class Pecsdemo {
static <T> void copy(
Iterable<? extends T> source,
Collection<? super T> target) {
for (T value : source) {
target.add(value);
}
}
public static void main(String[] args) {
List<Integer> source = List.of(1, 2);
List<Number> target = new ArrayList<>();
copy(source, target);
System.out.println(target);
// 输出:[1, 2]
}
}有界类型参数:限制可用操作
上界让方法安全调用 Comparable;空列表仍是调用方前置条件,应在真实 API 中先校验。
// 语义:上界让方法安全调用 Comparable。
import java.util.List;
public class BoundedTypeDemo {
static <T extends Comparable<? super T>> T max(List<? extends T> values) {
T result = values.get(0);
// 关键变化:T result = values.get(0);;读取 values 的索引 0 元素并写入 result;values 内容不变。
for (T value : values) {
if (value.compareTo(result) > 0) {
result = value;
}
}
return result;
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 1, 4)));
// 输出:4
}
}不常用但需要知道
使用原始类型:只在兼容旧 API 时保留
原始类型绕过泛型检查,可能把错误推迟到运行时;新代码应使用参数化类型,并把必要的 suppress 警告限制在最小边界。
import java.util.ArrayList;
import java.util.List;
public class RawTypeDemo {
public static void main(String[] args) {
List raw = new ArrayList();
raw.add("java");
@SuppressWarnings("unchecked")
List<String> names = raw;
System.out.println(names.get(0));
// 输出:java
}
}泛型数组:使用数组构造器或集合替代
不能直接 new T[],因为 T 通常被擦除;需要数组时让调用方提供数组构造器,动态结果优先返回 List<T>。
import java.util.Arrays;
public class GenericArrayBoundaryDemo {
static <T> T[] copy(T[] source, java.util.function.IntFunction<T[]> factory) {
return Arrays.copyOf(source, source.length, factory);
// 关键变化:return Arrays.copyOf(source, source.length, factory);;调用 copyOf,实参为 source, source.length, factory;复制 source 内容并按 factory 创建运行时类型;返回新数组。
}
public static void main(String[] args) {
String[] values = copy(new String[]{"a"}, String[]::new);
System.out.println(values[0]);
// 输出:a
}
}了解类型擦除对运行时判断的影响
运行时只能判断它是 List,不能直接判断 List<String>;元素类型安全主要由编译器在源代码阶段保证。
import java.util.ArrayList;
import java.util.List;
public class ErasureDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
System.out.println(names instanceof List);
// 输出:true
}
}简单案例
import java.util.ArrayList;
import java.util.List;
public class GenericsOverviewDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("java");
String first = names.get(0);
// 关键变化:String first = names.get(0);;读取 names 的索引 0 元素并写入 first;names 内容不变。
System.out.println(first);
// 输出:java
}
}参数化类型让读取结果直接是 String,避免把 Object 手动强转为 String。
易混点
- List<Integer> 与 List<Number> 不协变;元素有继承关系不等于容器有继承关系。
- extends 主要读取,super 主要写入;? extends 类型通常不能添加非 null 值。
- 泛型擦除不代表泛型无用,编译期检查仍是主要收益。
- 原始类型、未检查强转和不安全 varargs 可能造成堆污染。
课后小问
为什么 copy 的 source 使用 ? extends T、target 使用 ? super T? 答案:source 是生产者只读出 T,target 是消费者可以接收 T。 解析:这种方向让 List<Integer> 能复制到 Collection<Number>,同时阻止向来源列表写入错误类型。
为什么不能直接 new T[]? 答案:泛型类型参数通常在运行时被擦除,数组却需要知道实际元素类型。 解析:让调用方提供 IntFunction<T[]> 或改用 List<T>,可以把运行时类型信息放在安全边界上。
本节小结
- 泛型通过编译期类型关系减少强转,常见 API 使用类型参数和通配符表达方向。
- PECS 用 extends 表示生产者,用 super 表示消费者。
- 有界参数可以限制可用操作,原始类型和泛型数组则是兼容边界。
- 类型擦除限制运行时判断,但不削弱编译期类型检查价值。
快速回顾
- 能定义泛型类和泛型方法。
- 能用 PECS 判断 extends 与 super。
- 能解释泛型不变性和类型擦除。
- 能说明泛型数组、原始类型和堆污染的风险。
阿源的知识库