1. 什么是泛型擦除?Java 泛型是编译期还是运行期存在的?

Java 的泛型是编译期机制,运行期不存在:编译后类型参数被擦除(erasure)为原始类型 Object 或上界。

擦除带来的限制:不能 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 原则)

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,分两大支:

设计观点:受检异常强迫调用方处理可预期异常(文件不存在);非受检异常代表编程错误(空指针),不该被 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。

运行结果判断
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

🎤 常见面试追问

  1. 泛型擦除后,运行时怎么拿到类型信息?——反射的 getGenericType/getActualTypeArguments 能取回泛型签名(存于 class 文件的 Signature 属性)。Gson/Jackson 的 TypeToken 就是这么干的。
  2. 为什么不能 new T()?——擦除后 T 变成 Object/上界,运行时不知道具体类型,无法调用对应构造器;而且 T 无类型信息,JVM 无法保证安全。
  3. Spring 的 @Transactional 为什么不回滚受检异常?——设计约定:受检异常代表"可预期的业务失败"(如 IOException),默认不视为事务失败;RuntimeException 代表"程序缺陷"才回滚。可 rollbackFor 指定。
  4. finally 里能修改 return 的值吗?——基本类型:修改不影响已暂存的返回值;引用类型:修改对象字段会影响(返回的是引用);finally 里 return 会覆盖一切(禁止)。
  5. try-with-resources 关闭顺序?——按声明逆序关闭(后声明的先关);close 抛异常会作为 suppressed 附加到主异常。

📖 名词解释(本页术语)

术语 大白话解释
泛型给类型加"参数":List<String> 表示只装 String 的列表。编译期检查类型安全,运行期被擦除。
类型擦除编译后泛型参数被抹掉(变成 Object 或上界),class 文件里没有 List<String> 只有 List——这是 Java 泛型的实现方式。
通配符? extends T(上界:只能读)和 ? super T(下界:只能写),解决泛型不变性带来的限制。
PECS 原则Producer Extends, Consumer Super:往外读数据用 extends,往里放数据用 super——JDK 源码大量使用。
受检异常(Checked)编译器强制处理的异常(IOException 等),不 try-catch 或 throws 就编译不过。代表可预期的失败。
非受检异常(RuntimeException)编译器不强制处理的异常(空指针、越界等),大多是编程缺陷。Spring 事务默认只对它们回滚。
try-with-resources自动关闭资源的语法:try (资源声明) { },编译后自动 close,异常不互相覆盖(suppressed 机制)。
Error vs ExceptionError = JVM 级严重问题(OOM、栈溢出),程序一般处理不了;Exception = 程序可处理的异常。都在 Throwable 下。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。