⚠️ 泛型与异常
类型擦除与桥方法 · 通配符与 PECS · 受检/非受检异常 · try-with-resources · finally 与 return 的坑
1. 什么是泛型擦除?Java 泛型是编译期还是运行期存在的?
Java 的泛型是编译期机制,运行期不存在:编译后类型参数被擦除(erasure)为原始类型 Object 或上界。
List<String>和List<Integer>编译后都是List,class 文件里只有原始类型 + 类型擦除符号- 读取元素时的强转是编译器自动插入的(checkcast 指令)
- 边界擦除:
<T extends Comparable<T>>擦除为 Comparable
擦除带来的限制:不能 new T()、不能建泛型数组 new T[10]、不能 instanceof List<String>、静态字段不能是泛型(所有实例共享一份,类型无法统一)。
桥方法:擦除后保持多态
class Parent {
public Object get() { ... } // 擦除后
}
class Child extends Parent {
@Override
public String get() { ... } // 协变返回
// 编译器自动生成桥方法,维持父类签名,内部转调 String 版本
public bridge Object get() { return this.get(); }
}
🎯 面试要点
- 泛型提供了编译期类型安全 + 避免强转,代价是运行期无类型信息
- 延伸问:泛型和反射 —— 运行时可通过 getGenericType 取回类型参数(Gson/Jackson 的 TypeToken 就是这么干的)
2. ? extends T 和 ? super T 的区别?(PECS 原则)
List<? extends T>(上界通配符):元素是 T 或 T 的子类。只能读(读出的都是 T),不能写(编译器无法确定具体子类型)。用于"生产者":从容器取数据。List<? super T>(下界通配符):元素是 T 或 T 的父类。只能写(写入 T 一定合法),读出的只能是 Object。用于"消费者":往容器放数据。- PECS = Producer Extends, Consumer Super:读用 extends,写用 super。
PECS 应用:Collections.copy 的签名
public static <T> void copy(
List<? super T> dest, // 消费者:往里写
List<? extends T> src) { // 生产者:往外读
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i)); // 从 src 读,往 dest 写
}
}
// 使用:把 List<String> 拷到 List<Object> 合法
List<String> src = ...;
List<Object> dest = ...;
copy(dest, src);
🎯 面试要点
- 无界通配符
List<?>= 未知类型,只能读 Object,常用于只读入参 - List<Object> 与 List<String> 没有继承关系(泛型不变性),通配符是绕开它的手段
- JDK 源码里大量 PECS 签名:Collections.copy、Stream.collect 等
3. Java 异常体系?受检异常和非受检异常的区别?
体系:顶层 Throwable,分两大支:
- Error:JVM 层面的严重问题,程序无法处理 — OutOfMemoryError、StackOverflowError、NoClassDefFoundError
- Exception:程序可处理的异常
- 受检异常(Checked):编译器强制处理(try-catch 或 throws 声明)— IOException、SQLException。编译期就必须处理。
- 非受检异常(Unchecked / RuntimeException):编译器不强制 — NullPointerException、ClassCastException、IllegalArgumentException。大多是编程缺陷。
设计观点:受检异常强迫调用方处理可预期异常(文件不存在);非受检异常代表编程错误(空指针),不该被 catch 吞掉。现代框架(Spring)倾向于:业务异常用运行时异常,避免异常处理污染调用链。
受检 vs 非受检
// 受检异常:不 catch 或 throws 就编译不过
public void readFile(String path) throws IOException {
Files.readAllBytes(Path.of(path));
}
// 非受检异常:可抛可不抛,通常由参数校验触发
public int divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("除数不能为 0");
return a / b;
}
🎯 面试要点
- Spring 事务回滚的坑:@Transactional 默认只对 RuntimeException 回滚,受检异常不触发回滚(除非 rollbackFor 指定)
- catch 异常后要么处理要么抛出,禁止 catch 了却什么都不做(吞异常)
- 异常也耗时:new Throwable 会填充调用栈(fillInStackTrace),高并发热点路径避免用异常控制流程
4. try-with-resources 的原理?为什么比手动 finally 更可靠?
try-with-resources 要求资源实现 AutoCloseable 接口。编译期自动生成 close() 调用,且关闭顺序是声明逆序;若 close 也抛异常,会以原异常为主(close 的异常作为 suppressed 附加)。
对比手动写法的问题:手写 try-catch-finally 时,若 try 中抛 A 异常、finally 中 close 抛 B 异常,B 会覆盖 A,原始异常丢失,排查困难。try-with-resources 从语法层面解决。
等价写法:try-with-resources 是语法糖
// 写法一:try-with-resources(推荐)
try (InputStream in = Files.newInputStream(path);
BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
// 读取...
} // 自动关闭:先关 reader,再关 in(逆序)
// 写法二:编译后等价于——close 放在 finally,且异常可叠加
InputStream in = ...;
Throwable primary = null;
try {
// 读取...
} catch (Throwable t) { primary = t; throw t; }
finally {
try { in.close(); }
catch (Throwable t) {
if (primary != null) primary.addSuppressed(t); // 附加而非覆盖
}
}
🎯 面试要点
- AutoCloseable 是函数式接口:
void close() throws Exception - 多个资源逆序关闭:后声明的先关(类似栈)
- 延伸:finally 里写 return 会吞掉 try 中异常并覆盖返回值——永远不要在 finally 里 return
5. finally 中 return 的陷阱(必问)
铁律:finally 永远先于 return 执行;finally 中的 return 会覆盖 try 中的 return。
- try 中 return 的值会先存入局部变量槽,然后执行 finally;finally 对基本类型的修改不影响已暂存的值
- finally 中若有 return,直接吞掉 try 的返回值并替代之(且 try 抛的异常也会被吞掉!)
- finally 中修改引用类型对象的字段,会影响返回值(因为返回的是引用)
运行结果判断
public static int test() {
int i = 0;
try {
return i; // 先暂存 0
} finally {
i = 10; // 修改不影响已暂存的值
}
}
// 返回 0
public static int test2() {
try { return 1; }
finally { return 2; } // finally 的 return 覆盖 try 的 return
}
// 返回 2(同时吞掉 try 中抛出的任何异常)
🎯 面试要点
- finally 的职责只有清理资源,禁止写 return
- 常见坑:try-with-resources 与 finally 并存时先执行 try-with-resources 的 close