一、异常整体概念与分类
被问到异常处理,先说整体概念和分类(Error / Exception,Checked / Unchecked)。
Throwable 继承体系
Throwable (所有错误和异常的祖先)
│
├── Error (严重错误, 程序通常无法恢复, 不需要捕获处理)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ... (虚拟机错误、系统资源耗尽等)
│
└── Exception (程序可处理的异常)
│
├── Checked Exception (检查型异常/编译时异常)
│ ├── IOException (文件操作)
│ ├── FileNotFoundException
│ ├── ClassNotFoundException
│ ├── SQLException
│ └── ... (其他需要在编译时处理的异常)
│
└── Unchecked Exception (RuntimeException, 非检查型异常/运行时异常)
├── NullPointerException (NPE - 面试最爱考!)
├── ArrayIndexOutOfBoundsException (数组越界)
├── ClassCastException (类型转换错误)
├── IllegalArgumentException (非法参数)
├── ArithmeticException (算术异常, 如除以 0)
└── ... (其他由程序逻辑错误导致的异常)
Exception 和 Error 的区别
- Exception: 程序本身可以预见的、可以捕获处理的异常情况。代表了程序可以恢复的问题。
- Error: 非常严重的、通常由 JVM 或系统底层资源导致的错误(如内存溢出、栈溢出、虚拟机崩溃)。程序不应该尝试捕获这些错误,因为它们通常意味着程序已无法正常运行。
Checked vs Unchecked (Runtime) 异常
这是重中之重——编译器如何对待它们:
- Checked(检查型):
Exception中除RuntimeException及其子类之外的部分。- 特点: 编译器强制检查和处理。如果方法里可能抛出或内部方法抛出了检查型异常,你必须:
- 要么用
try-catch捕获并处理掉它; - 要么在方法签名上用
throws声明会抛出此异常(把"锅"甩给调用者)。
- 要么用
- 特点: 编译器强制检查和处理。如果方法里可能抛出或内部方法抛出了检查型异常,你必须:
- Unchecked / Runtime(运行时异常):
Error和RuntimeException及其子类。- 特点: 编译器不强制要求处理(可以不用
try-catch或throws)。通常由程序逻辑错误导致(如空指针、数组越界、错误的类型转换、无效参数等)。 - 面试常见题:
NullPointerException、ArrayIndexOutOfBoundsException为什么不强制要求try-catch?
- 特点: 编译器不强制要求处理(可以不用
记忆口诀: Error 真崩溃,Exception 可应对。Exception 分两种,Checked 编译要声明,Runtime 不必提。检查要动手(处理或声明),运行靠逻辑。
二、核心概念与关键词(必背!)
try
标记可能抛出异常的代码块(危险区)。
catch
捕获并处理特定类型的异常。可以有多个 catch 块。
- 面试重点:
catch块顺序很重要!必须先捕获子类异常,再捕获父类异常。如果父类在子类前面,子类异常永远捕获不到。 - JDK 7+ 多重捕获:
catch (IOException | SQLException e) { ... }可以用|捕获多个不相关的异常类型。
finally
无论是否发生异常,无论是否被捕获,该块内的代码都会执行!
- 核心用途: 释放资源(关闭文件流、数据库连接、网络套接字等),确保资源不被泄露。
- 面试重点 1:
finally和return的执行顺序?- 如果
try或catch中有return,会先执行return语句的计算(将返回值放入栈),然后执行finally块,最后才真正结束方法并返回之前计算好的值。 - 如果
finally里也有return,会覆盖掉try或catch中的返回值!(危险!应避免这样写!)
- 如果
throw vs throws
throws: 用在方法签名中,声明该方法可能会抛出哪些检查型异常(Checked Exception)。- 告诉调用者:"我这个方法可能会出 XXX 问题,请你做好准备(要么
try-catch处理我,要么你也throws往上抛)"。 - 例:
public void readFile() throws IOException { ... }
- 告诉调用者:"我这个方法可能会出 XXX 问题,请你做好准备(要么
throw: 在方法内部,主动抛出一个异常对象。- 例:
throw new NullPointerException("参数不能为空!");
- 例:
三、try-with-resources(JDK 7+)
- 用来自动关闭实现了
AutoCloseable接口的资源(如流、连接)。 - 语法:
try (资源声明1; 资源声明2) { ... }不需要显式调用close(),也不用手写finally关闭,编译器会生成正确的关闭逻辑。 - 优势: 简洁、安全、避免资源泄露。
- 面试高频: 对比传统
try-catch-finally和try-with-resources,说出好处。
// 编译器自动生成关闭逻辑,安全且简洁
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 业务逻辑
} catch (IOException e) {
e.printStackTrace();
}
四、最佳实践与常见问题
1. 自定义异常
- 为什么用? 创建特定于业务逻辑或领域的错误类型,提高代码可读性和可维护性。
- 如何做? 继承
Exception(创建检查型异常)或RuntimeException(创建非检查型异常)。 - 要点:
- 提供无参构造器。
- 提供带
String message的构造器(传递错误信息)。 - 提供带
Throwable cause的构造器(保留原始异常信息)。 - 通常重写
toString()或getMessage()提供更友好信息。
- 面试题: 如何设计一个自定义异常?
2. 异常处理的性能开销
- 创建异常对象(包含栈轨迹)有开销。
try块本身开销很小。- 核心原则: 不要用异常来控制正常流程逻辑(尤其是非检查型异常)! 异常只该用于处理异常情况。用
if检查预防NullPointerException比捕获它高效得多!
3. 为什么异常信息会丢失或被"吞掉"?
- 错误示例 1:
catch块里只写了e.printStackTrace();但没有后续处理或再次抛出。 - 错误示例 2:
catch块捕获了一个异常 A,在处理过程中又抛出一个异常 B,但没有把 A 作为 B 的 cause(new MyException("...", e)),导致异常链丢失。 - 错误示例 3:
finally块里抛出的异常会覆盖掉try或catch块里的异常(如果前者没有被捕获)。
五、高频面试题速查
finally块的作用是?
确保资源释放(如FileInputStream.close())。try-catch-finally块中,如果try里return了,finally还执行吗?
执行! 它会在return语句计算后、方法真正返回前执行。- 如果
finally里也有return会怎样?
会覆盖try或catch中的返回值!(危险!避免这样写!) - 异常处理的性能与最佳实践?
finally 关资源,try-resources 更简单,别吞异常、别滥用异常控制流程。 - 声明与抛出?
throw抛出动作,throws声明风险。
快速总结口诀
结构: Throwable,Error 和 Exception 分两边;Exception 两兄弟,Checked 要管,Runtime 靠自律。
块: try 标记危险区,catch 处理抓异常,多 catch 子类先,finally 必执行清资源。
检查与非检查: 检查异常要管(处理或声明),运行时靠逻辑(防 NPE、防越界)。

