第 05 期:异常、泛型与反射------类型擦除的成本与优化
1) 面试原题
- Java 泛型是如何实现的?为什么叫"类型擦除"?
- 泛型在运行时是否保留类型信息?为什么
List<String>
和List<Integer>
在运行时是同一个类? - 反射的性能问题来自哪里?如何优化?
- 异常体系的设计原则是什么?Checked 与 Unchecked 异常的区别?
- 如何在面试中解释"泛型 + 反射 + 异常"的底层逻辑?
2) 第一性拆解
约束(Constraints)
- Java 泛型设计目标:兼容旧代码(JDK 1.4 之前),避免破坏已有类库。
- JVM 字节码没有泛型概念,只有原始类型(Raw Type)。
- 反射必须在运行时动态解析类结构,无法提前编译优化。
- 异常必须支持强类型检查 (Checked)与灵活传播(Unchecked)。
成本模型(Cost Model)
- 泛型擦除:编译期检查,运行时擦除 → 无额外内存,但丢失类型信息。
- 反射:动态查找 + 安全检查 + 访问控制 → 比直接调用慢 10~100 倍。
- 异常:创建栈轨迹(StackTrace)成本高,频繁抛异常会拖慢性能。
最小原语(Primitives)
- 泛型擦除 :
List<String>
编译后变成List
,插入强制类型转换。 - 桥接方法(Bridge Method):为保持多态,编译器生成额外方法。
- 反射调用 :
Method.invoke()
走动态分派,需安全检查 + 参数装箱。 - 异常表(Exception Table):字节码中记录异常处理范围,JVM 根据 PC 寄存器跳转。
可验证结论(Check)
- 用
javap -c
反编译泛型代码,观察类型擦除与强制转换。 - 用 JMH 对比反射调用 vs 直接调用性能差异。
- 打印异常栈轨迹,观察性能开销。
3) 泛型的本质:编译期检查 + 运行时擦除
示例:泛型擦除
java
public class GenericDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);
System.out.println(s);
}
}
反编译(javap -c GenericDemo
):
plain
0: new #2 // class java/util/ArrayList
...
16: invokevirtual #3 // Method java/util/List.get:(I)Ljava/lang/Object;
19: checkcast #4 // class java/lang/String
说明:
- 编译器插入
checkcast
保证类型安全。 - 运行时只有
List
,没有List<String>
,这就是"类型擦除"。
为什么这样设计?
- 保持与 JDK 1.4 之前的类库兼容。
- 避免 JVM 层面修改(否则需要字节码和类加载器大改)。
桥接方法(Bridge Method)示例
java
class Parent<T> {
T get() { return null; }
}
class Child extends Parent<String> {
@Override
String get() { return "Hi"; }
}
反编译 Child
:
plain
public java.lang.String get();
public java.lang.Object get(); // 编译器生成的桥接方法
原因 :保持多态,JVM 调用时仍能找到 Object
版本。
4) 泛型与反射的冲突
- 泛型擦除后,反射无法直接获取参数化类型:
java
List<String> list = new ArrayList<>();
System.out.println(list.getClass()); // class java.util.ArrayList
- 解决方案 :通过
Type
API 获取泛型信息(仅限编译期注解或运行时保留的类型签名):
java
Field f = MyClass.class.getDeclaredField("list");
Type t = f.getGenericType(); // java.lang.reflect.ParameterizedType
5) 反射性能问题与优化
性能瓶颈
- 每次
Method.invoke()
都要做:- 访问检查(
setAccessible(true)
可跳过) - 参数装箱/拆箱
- 动态分派
- 访问检查(
优化手段
- 缓存 Method 对象,避免重复查找。
- 关闭安全检查 :
method.setAccessible(true)
(仅在可信环境)。 - MethodHandle / VarHandle(JDK 7+):更接近直接调用,性能优于反射。
- 代码生成:如 ASM、Javassist、ByteBuddy,生成直接调用字节码。
JMH 对比(示意)
- 直接调用:~1 ns
- 反射调用:~100 ns
- MethodHandle:~10 ns(取决于绑定方式)
6) 异常体系与设计原则
- Checked Exception :必须显式处理(
IOException
、SQLException
),用于可恢复错误。 - Unchecked Exception :继承
RuntimeException
,用于编程错误(NullPointerException
、IllegalArgumentException
)。 - Error :严重错误(
OutOfMemoryError
),通常不可恢复。
设计原则
- Checked 用于业务逻辑可恢复场景;
- Unchecked 用于编程错误;
- 不要滥用异常做流程控制(性能差 + 可读性差)。
性能注意
- 异常创建会捕获栈轨迹,成本高;频繁抛异常会拖慢性能。
- 优化:避免在热点路径用异常控制逻辑;必要时关闭栈轨迹(
new Exception("msg", null, false, false)
)。
7) 速答卡(30 秒)
- 泛型实现 :编译期检查,运行时擦除 →
List<String>
和List<Integer>
在运行时是同一个类。 - 桥接方法:保持多态,编译器生成额外方法。
- 反射性能差 :动态分派 + 安全检查 + 装箱;优化用
MethodHandle
或代码生成。 - 异常分类:Checked(必须处理,可恢复)、Unchecked(编程错误)、Error(不可恢复)。
- 面试加分:提到泛型擦除的历史原因(兼容旧代码),以及反射与泛型冲突的解决方案。
8) 作业(可验证)
- 用
javap -c
反编译泛型代码,标注checkcast
出现位置。 - 写一个 JMH 基准测试,对比直接调用、反射调用、MethodHandle 调用性能。
- 用
ParameterizedType
获取泛型信息,解释为什么运行时仍能拿到签名。 - 写一段代码,演示异常栈轨迹关闭的性能差异。
9) 面试加分点
- 泛型擦除的历史原因(兼容性)。
- 桥接方法的作用(保持多态)。
- 反射性能优化手段(MethodHandle、代码生成)。
- 异常体系的设计哲学(Checked vs Unchecked)。
- 泛型与反射冲突的解决方案(
Type
API)。