面试中关于异常,主要考察 体系结构执行流程陷阱 两大块。

1. 异常体系结构 (Exception Hierarchy)

一句话总结: 所有的异常都有一个共同的祖先 Throwable

  • Throwable:所有错误和异常的超类。
    • Error (错误): 系统级问题
      • 通常是 JVM 出了问题(如内存溢出 OutOfMemoryError、栈溢出 StackOverflowError)。
      • 特点: 程序无法处理,一旦发生只能挂掉,不用 catch。
    • Exception (异常): 程序级问题
      • 程序可以捕获并处理的问题。分为两大类:
        1. 受检异常 (Checked Exception):
          • 编译期强制检查。如果不处理(try-catch 或 throws),代码编译不过。
          • 例子: IOException, SQLException, ClassNotFoundException
          • 场景: 外部不可控因素(文件不存在、网络断开)。
        2. 非受检异常 (Unchecked/Runtime Exception):
          • 运行期异常。编译时不检查。
          • 例子: NullPointerException (空指针), IndexOutOfBoundsException (数组越界), ArithmeticException (/0)。
          • 场景: 通常是代码逻辑写错了,应该修代码而不是 catch。

2. 异常处理关键字

  • try:包裹可能出得代码。
  • catch:捕获异常并处理。
  • finally无论是否发生异常,都一定会执行的代码块。通常用于释放资源(关闭 IO 流、数据库连接)。
  • throw (动作):写在方法体内部。用于手动抛出一个异常对象。
    • throw new RuntimeException("挂了");
  • throws (声明):写在方法签名上。声明该方法可能会抛出某些异常,交给调用者处理。
    • public void readFile() throws IOException { ... }

3. 经典面试陷阱题

陷阱一:throwthrows 的区别?

  • 位置不同: throw 在方法内;throws 在方法名后面。
  • 作用不同: throw 是真的扔出一个异常;throws 只是告诉别人“这里可能会炸”,是一种风险预警。

陷阱二:try-catch-finally 的执行顺序(带 return) —— 必考

问题: 下面代码返回什么? Java

public String test() {
    try {
        return "a";
    } finally {
        return "b";
    }
}

答案: 返回 “b”。 原理:

  1. finally 块的代码优先级极高
  2. 即使 try 块里执行到了 return,程序准备返回 “a” 之前,必须先去执行 finally
  3. 如果 finally 里也有 return,它会覆盖try 里的返回值。

陷阱三:什么情况下 finally 不会执行?

只有一种极端情况:trycatch 中调用了 System.exit(0)(直接杀掉了 JVM 进程)。除此之外,finally 就算天塌下来也会执行。

4.为什么有些异常抛出了,却不需要在方法签名上写 throws 声明?

1. 核心结论

因为该异常属于 RuntimeException(非受检异常)

  • 强制要求: 只有 Checked Exception(受检异常,如 IOException)必须在方法名后声明 throws,否则编译器不让过。

  • 非强制: RuntimeException 及其子类,你可以随心所欲地 throw,编译器不会逼你写 throws

2. 深度逻辑:为什么要这么设计?

  • Checked Exception(必须要 throws):

    • 性质: “预期内、可恢复”的外部故障。

    • 场景: 比如网络突然断了、文件被删了。这些事很可能发生,调用者必须写好“备选方案”(try-catch)。

    • 比喻: 就像登山领队(编译器)强制你必须带急救包,因为山上确实可能会有人擦伤,这是登山计划的一部分。

  • Runtime Exception(不用 throws):

    • 性质: “不可恢复”的逻辑断层或环境灾难。

    • 场景:

      1. 低级 Bug: 空指针、除以零。这是开发者逻辑不严密。

      2. 配置/环境崩坏(重点!): 比如你查数据库发现核心规则配置没了。代码没法跑了,必须立刻停下来(Fail-fast 机制)。

    • 比喻: 就像你走路撞墙(Bug),或者脚下的路突然裂开了个大缝(数据库断了/配置没了)。这种时候戴头盔(try-catch)已经没用了,必须停下来修路,而不是假装没事继续走。

3. 为什么不强制声明 Runtime Exception?

  • 代码整洁度: 如果空指针(NPE)也要强制 throws,那 Java 每一个方法后面都要跟上一长串 throws,代码会变得极其臃肿。

  • 解耦: 现代框架(如 Spring)倾向于把所有错误包装成 RuntimeException,这样中间层的代码就不必层层声明抛出,只需在最外层(全局异常处理器)统一抓取处理。


总结表格

类型检查时机开发者处理要求代表人物潜台词
Error运行期无法处理,直接原地爆炸OOM, StackOverflow“别救了,程序断气了”
Checked编译期必须 try-catchthrowsIOException, SQLException“这事常发生,想好怎么收场”
Runtime运行期不强制处理,通常建议抛出NullPointer, IllegalArgument“要么代码有 Bug,要么环境炸了”

“Checked Exception 是一种理想主义的设计,但在大型分布式架构中,它增加了系统的耦合度。现代实践更倾向于使用语义明确的 RuntimeException(非受检异常),结合全局异常处理器来实现解耦。”