12.3 (下):类型擦除与运行时边界
完成不变性、通配符与 PECS后,再用约 40 分钟检查泛型跨过编译边界时发生了什么。你会实际观察擦除、桥接方法、可具体化类型、数组冲突与
Class<T>类型令牌。
工具箱到了运行时,还认得 String 吗
老陈看着 Box<String> 和 Box<Integer> 问:“既然编译器能阻止放错东西,程序运行以后,这两个箱子还是两种类吗?”
它们在源码中携带不同类型关系,运行时却通常共享同一个 Box.class。这不表示泛型“只是注释”:编译器已经完成类型检查,必要的转换和桥接方法也写进了 class 文件。需要分清的是,哪些信息参与编译,哪些信息仍可在运行时查询。
1. 擦除发生在检查之后
没有显式上界的 T 通常擦除到 Object:
final class Box<T> {
T get() { /* ... */ }
}get 的字节码描述符返回 Object。调用 Box<String>.get() 时,编译器在调用处插入到 String 的检查转换。若声明为 <T extends Number>,T 擦除到 Number;有多个上界时使用最左侧上界。
Box<String> 与 Box<Integer> 因而共享运行时类:
boolean sameClass =
new Box<>("Hammer").getClass().equals(new Box<>(42).getClass());class 文件仍可用 Signature 等属性保存声明上的部分泛型信息,反射能读取字段、方法或父类声明里的类型参数。普通 Box 对象本身却通常不知道调用方当初写的是 String 还是 Integer。
2. 桥接方法维持擦除后的多态
工具箱离开源码工作台、进入 class 文件后,接口上的 T 已经擦成较宽的类型。可子类仍答应返回 String。编译器会暗中补上一块转接板,让旧接口和更具体的实现仍能扣在一起。
interface Source<T> {
T get();
}
final class StringSource implements Source<String> {
public String get() {
return "Hammer";
}
}Source 的 T 擦除到 Object,实现方法却返回 String。编译器会为 StringSource 生成一个 synthetic bridge method,使擦除后的 Object get() 仍能分派到源码里的 String get()。
可以从命令行观察:
javac --release 17 StringSource.java
javap -v StringSource也可以在反射中检查 Method.isBridge()。覆盖率或调用栈偶尔出现源码中没有的方法,不必立刻怀疑字节码被篡改,先看它是不是编译器生成的桥接方法。
3. 擦除会让某些重载撞在一起
如果两只箱子只靠 List<String> 和 List<Integer> 区分,擦掉标签后,货架上只剩两个同名的 List 入口。JVM descriptor 无法再判断该走哪一个。
以下两个方法不能同时声明:
void process(List<String> values) {}
void process(List<Integer> values) {}它们擦除后都是 process(List),JVM 方法描述符无法区分。可以改用不同方法名、增加类型确实不同的非泛型参数,或把逻辑合并成一个泛型方法。仅靠返回类型不同也不能构成 Java 重载。
4. 可具体化类型才能做完整运行时检查
运行时保留足够信息的类型称为 reifiable type。基本类型、非泛型类、raw type、无界通配符参数化类型,以及由可具体化组件构成的数组,是常见例子。
if (value instanceof List<?>) {
List<?> list = (List<?>) value;
}
// value instanceof List<String> // 编译失败JVM 能确认对象是 List,却不能凭这个检查保证所有元素都是 String。若接口要求这项保证,必须遍历元素,并明确遇到不兼容元素时是拒绝整个输入还是报告具体位置。
同一边界也解释了这些限制:
// new T()
// T.class
// new T[10]
// new List<String>[10]编译器不知道 T 的构造器,也没有一个代表任意 T 的运行时 Class 对象。泛型类还不能直接或间接继承 Throwable,否则 catch 无法根据已擦除的类型实参区分异常。
5. 数组协变,泛型不变
数组在运行时保留组件类型:
Object[] values = new String[1];
values[0] = 42; // ArrayStoreException赋值能编译,因为 String[] 是 Object[] 的子类型;实际写入时,数组对象知道自己的组件是 String,因此拒绝整数。泛型则在编译期拒绝 List<Integer> 到 List<Number> 的赋值。
需要从泛型集合得到数组时,把运行时组件类型交给调用者:
static <T> T[] toArray(
List<T> values,
java.util.function.IntFunction<T[]> factory) {
return values.toArray(factory);
}
String[] tools = toArray(List.of("Hammer"), String[]::new);一般的可变序列优先使用 List<T>。只有 API 边界确实要求数组时,才承担组件类型和数组协变带来的额外规则。
6. raw type、unchecked cast 与泛型 varargs
类型实参必须是引用类型,所以使用 List<Integer>,不能写 List<int>。装箱让整数进入泛型集合,也会带来转换和内存成本;大量数值计算需要基准测试,而不是默认标准集合总够快。
下面的泛型 varargs 会创建擦除后的数组视图:
static <T> List<T> combine(List<T>... groups) {
// ...
}编译器可能报告 possible heap pollution。raw type、未经验证的 unchecked cast,以及泄漏或写入泛型 varargs 数组,都会让堆中对象违背静态类型承诺。
@SafeVarargs 是作者对方法体安全性的承诺,不是关闭警告的按钮。只有确认方法既不写入不兼容值,也不把 varargs 数组交给不可信代码时才能使用。能改成 List<List<T>> 时,普通集合参数通常更容易审查。
7. Class<T> 把一层运行时类型交回 API
有些仓库检查必须发生在运行时。与其假装箱子自己记得完整泛型实参,不如让调用者递上一枚 Class<T> 类型令牌。它能核对一层具体 runtime class,却恢复不了 List<String> 里嵌套的 String。
调用者可以显式提供 Class token:
final class TypeRegistry {
private final Map<Class<?>, Object> values = new HashMap<>();
<T> void put(Class<T> type, T value) {
values.put(type, type.cast(value));
}
<T> Optional<T> find(Class<T> type) {
return Optional.ofNullable(values.get(type)).map(type::cast);
}
}Class.cast 在注册边界做运行时检查,find(String.class) 的返回类型也随 token 变为 Optional<String>。这种模式适合按运行时类注册配置、处理器或服务。
它只能表达运行时 Class。List<String> 和 List<Integer> 都对应 List.class,无法取得 Class<List<String>>。框架要保存嵌套类型时,通常使用 Type、type token 子类或显式类型描述对象;下一章讲反射时会再遇到这个边界。
8. 拆开 class 文件验证三道边界
这一回不能只运行 main。先让 javac 检查源码,再用 javap 打开 class 文件寻找 bridge method,最后用运行时断言检查 type token。老陈的工具箱从源码走到 JVM,每一站留下的类型信息都不同。
先准备临时目录:
chapter12_erasure_root=$(mktemp -d)
readonly chapter12_erasure_root
mkdir -p "$chapter12_erasure_root/out"
cd "$chapter12_erasure_root"保存为 ErasureRuntimeDemo.java:
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public class ErasureRuntimeDemo {
public static void main(String[] args) {
Box<String> words = new Box<>("Hammer");
Box<Integer> counts = new Box<>(3);
check(words.getClass().equals(counts.getClass()),
"不同类型实参应共享同一个运行时类");
boolean bridgeFound = false;
for (Method method : StringSource.class.getDeclaredMethods()) {
if (method.isBridge() && method.isSynthetic()) {
bridgeFound = true;
}
}
check(bridgeFound, "编译器应生成 synthetic bridge method");
Source<String> source = new StringSource();
checkEquals("Hammer", source.get());
TypeRegistry registry = new TypeRegistry();
registry.put(String.class, "west");
registry.put(Integer.class, 3);
checkEquals(Optional.of("west"), registry.find(String.class));
checkEquals(Optional.of(3), registry.find(Integer.class));
checkEquals(Optional.empty(), registry.find(Long.class));
Map<Class<?>, Object> snapshot = registry.snapshot();
expect(UnsupportedOperationException.class,
() -> snapshot.put(Long.class, 9L));
Object value = java.util.List.of("Hammer");
check(value instanceof java.util.List<?>,
"无界通配符类型可以做 instanceof 检查");
System.out.println("共享运行时类:" + words.getClass().getSimpleName());
System.out.println("检测到桥接方法:" + bridgeFound);
System.out.println("类型注册:" + registry.find(String.class)
+ " / " + registry.find(Integer.class));
System.out.println("擦除与运行时边界检查通过");
}
interface Source<T> {
T get();
}
static final class StringSource implements Source<String> {
@Override
public String get() {
return "Hammer";
}
}
static final class Box<T> {
private final T value;
Box(T value) {
this.value = Objects.requireNonNull(value, "value");
}
T get() {
return value;
}
}
static final class TypeRegistry {
private final Map<Class<?>, Object> values = new HashMap<>();
<T> void put(Class<T> type, T value) {
values.put(type, type.cast(value));
}
<T> Optional<T> find(Class<T> type) {
return Optional.ofNullable(values.get(type)).map(type::cast);
}
Map<Class<?>, Object> snapshot() {
return Collections.unmodifiableMap(new HashMap<>(values));
}
}
static <T extends Throwable> T expect(
Class<T> type, Runnable action) {
try {
action.run();
} catch (Throwable failure) {
if (type.isInstance(failure)) return type.cast(failure);
throw new AssertionError(
"expected=" + type + ", actual=" + failure, failure);
}
throw new AssertionError("expected exception=" + type.getName());
}
static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
static void checkEquals(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError(
"expected=" + expected + ", actual=" + actual);
}
}
}javac --release 17 -Xlint:all -d out ErasureRuntimeDemo.java
java -cp out ErasureRuntimeDemo预期输出:
共享运行时类:Box
检测到桥接方法:true
类型注册:Optional[west] / Optional[3]
擦除与运行时边界检查通过保存记录后清理:
cd
ls -ld -- "$chapter12_erasure_root"
rm -r -- "$chapter12_erasure_root"离开工具箱前,分清两道边界
编译期先验证参数化类型关系,运行时再按擦除后的类和必要的检查转换执行。桥接方法维持多态,List<?> 允许检查“它是一个 List”,Class<T> 则让调用者显式交回一层类型信息;它们都不能凭空恢复 List<String> 的嵌套实参。
最后让编译器拒绝四种写法:按 List<String> 与 List<Integer> 重载、instanceof List<String>、new T[10]、泛型异常类。再用 javap -v 对照反射检测到的 bridge method,源码和字节码就接上了。
查阅 Java 17 的正式规则
- JLS 4.6:类型擦除。
- JLS 4.7:可具体化类型。
- JLS 4.12.2:heap pollution。
- JLS 15.20.2:运行时类型比较。
Method.isBridge与Class.cast。
下一步:注解与反射
泛型说明了编译器如何保留类型关系,又在运行时擦除大部分类型实参。下一章会看到另一条信息通道:注解把元数据写进声明,反射在运行时读取类、方法和字段。