文章目录
-
- 前言
- 一、异常是什么
-
- [1.1 异常的核心定义](#1.1 异常的核心定义)
- [1.2 异常不是错误流程的替代品](#1.2 异常不是错误流程的替代品)
- [二、Java 异常体系](#二、Java 异常体系)
-
- [2.1 `Throwable`、Error、Exception](#2.1
Throwable、Error、Exception) - [2.2 checked 异常和 unchecked 异常](#2.2 checked 异常和 unchecked 异常)
-
- [2.2.1 checked异常](#2.2.1 checked异常)
- [2.2.2 unchecked异常](#2.2.2 unchecked异常)
- [2.2.3 常见 unchecked 异常逐个理解](#2.2.3 常见 unchecked 异常逐个理解)
-
- [1. `NullPointerException`](#1.
NullPointerException) - [2. `ArithmeticException`](#2.
ArithmeticException) - [3. `NumberFormatException`](#3.
NumberFormatException) - [4. `IndexOutOfBoundsException`](#4.
IndexOutOfBoundsException) - [5. `IllegalArgumentException`](#5.
IllegalArgumentException)
- [1. `NullPointerException`](#1.
- [2.1 `Throwable`、Error、Exception](#2.1
- 三、try-catch-finally
-
- [3.1 try:放可能出问题的代码](#3.1 try:放可能出问题的代码)
- [3.2 catch:捕获并处理异常](#3.2 catch:捕获并处理异常)
- [3.3 finally:无论成功失败都执行](#3.3 finally:无论成功失败都执行)
- [3.4 不要在 finally 中 return](#3.4 不要在 finally 中 return)
- [四、throw 和 throws](#四、throw 和 throws)
-
- [4.1 throw:方法体里主动抛异常](#4.1 throw:方法体里主动抛异常)
- [4.2 throws:方法签名上声明异常](#4.2 throws:方法签名上声明异常)
- 五、自定义异常和异常链
-
- [5.1 为什么需要自定义异常](#5.1 为什么需要自定义异常)
-
- [5.1.1 直接抛出 RuntimeException 的问题](#5.1.1 直接抛出 RuntimeException 的问题)
- [5.1.2 自定义异常让错误具有业务名字](#5.1.2 自定义异常让错误具有业务名字)
- [5.1.3 自定义异常不是为了"多写一个类"](#5.1.3 自定义异常不是为了“多写一个类”)
- [5.2 自定义异常的基本写法](#5.2 自定义异常的基本写法)
-
- [5.2.1 继承 RuntimeException:定义 unchecked 业务异常](#5.2.1 继承 RuntimeException:定义 unchecked 业务异常)
- [5.2.2 继承 Exception:定义 checked 业务异常](#5.2.2 继承 Exception:定义 checked 业务异常)
- [5.3 什么是异常包装](#5.3 什么是异常包装)
- [5.4 异常链到底长什么样](#5.4 异常链到底长什么样)
- 六、try-with-resources
-
- [6.1 为什么需要 try-with-resources](#6.1 为什么需要 try-with-resources)
- [6.2 try-with-resources 的写法](#6.2 try-with-resources 的写法)
- [6.3 suppressed exception](#6.3 suppressed exception)
- 七、项目和面试如何考察
-
- [7.1 项目中常见用法](#7.1 项目中常见用法)
- [7.2 面试高频问题](#7.2 面试高频问题)
- [7.3 面试回答思路](#7.3 面试回答思路)
- 总结
前言
程序运行时并不总是一帆风顺。文件可能不存在,网络可能超时,用户可能输入非法数据,对象可能是 null。这些"计划之外的情况",就是异常处理要面对的问题。
异常就像程序道路上的警示牌:它告诉我们"这里出问题了",同时也给我们一个机会,决定是修复、提示、重试、回滚,还是把问题继续向上抛。
Java 的异常机制,就像给程序建立了一套"故障处理通道":正常情况下,代码沿着主流程向前执行;一旦出现异常,就可以通过 throw 发出问题,通过 catch 接住并处理,通过 throws 声明风险,还可以利用异常链保留最初的故障原因。
本文将从异常的基本概念开始,逐步讲解 Java 异常体系、try-catch-finally、throw 与 throws、自定义异常、异常链以及 try-with-resources。
一、异常是什么
1.1 异常的核心定义
异常是程序执行过程中出现的非正常情况。它会打断当前正常流程,把问题交给异常处理机制。
比如:
java
int result = 10 / 0;
这行代码会抛出:
text
java.lang.ArithmeticException: / by zero
如果没有异常处理,程序会中断。加上 try-catch 后,我们可以给用户一个更友好的提示。
java
public static String divideSafely(int left, int right) {
try {
return "result:" + (left / right);
} catch (ArithmeticException e) {
return "error:" + e.getMessage();
}
}
1.2 异常不是错误流程的替代品
异常应该处理"不正常情况",不应该替代普通业务判断。
推荐:
java
if (right == 0) {
return "除数不能为 0";
}
不推荐:
java
try {
return left / right;
} catch (ArithmeticException e) {
return 0;
}
能用正常判断解决的,就不要靠异常绕路。异常像消防通道,不是日常上下班电梯。
二、Java 异常体系
2.1 Throwable、Error、Exception
Java 中能被抛出的对象,必须是 Throwable 或它的子类。
整体结构可以简化为:
text
Throwable
├── Error // unchecked 异常
│ ├── OutOfMemoryError
│ └── StackOverflowError
│
└── Exception
├── IOException // checked 异常
├── SQLException // checked 异常
├── ClassNotFoundException // checked 异常
│
└── RuntimeException // unchecked 异常
├── NullPointerException
├── ArithmeticException
├── NumberFormatException
├── IndexOutOfBoundsException
└── IllegalArgumentException
Error 表示严重错误,比如 OutOfMemoryError、StackOverflowError。这类问题通常不是业务代码可以优雅恢复的。
Exception 表示程序中可以被处理的问题,比如文件不存在、参数不合法、网络读取失败等。
2.2 checked 异常和 unchecked 异常
Exception 又可以分成两类:
- checked 异常:编译器要求必须处理,要么
try-catch,要么throws。 - unchecked 异常:运行时异常,编译器不强制处理。
2.2.1 checked异常
所谓 checked,检查的是:
代码中某个操作可能抛出这种异常时,你有没有明确处理它。
例如,读取文件时,文件可能不存在、没有权限或读取失败。这些问题不是代码写错了,而是程序运行环境中本来就可能发生的情况。
常见 checked 异常:
IOExceptionClassNotFoundExceptionSQLException
例如代码:
java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileDemo {
public static void main(String[] args) {
String content = Files.readString(Path.of("user.txt"));
System.out.println(content);
}
}
这段代码无法通过编译,因为 Files.readString(...) 可能抛出 IOException,而代码没有说明如何处理它。
此时编译器会要求你二选一:
- 当前方法自己处理:使用try-catch
- 声明交给调用者处理:throws。
这就是 Java 中的 Catch or Specify Requirement :可能抛出 checked 异常的代码,必须捕获它,或者在方法声明中使用 throws 明确声明。否则代码不能通过编译。
2.2.2 unchecked异常
unchecked 异常主要指 RuntimeException 及其子类。
它们的特点是:
- 编译器不要求必须使用
try-catch; - 编译器不要求方法必须声明
throws; - 程序仍然可能在运行时抛出这些异常;
- 很多情况下表示代码逻辑、参数校验或
API使用方式存在问题。
常见 unchecked 异常:
NullPointerExceptionArithmeticExceptionNumberFormatExceptionIndexOutOfBoundsExceptionIllegalArgumentException
例如:
java
public class RuntimeDemo {
public static void main(String[] args) {
int a = 10;
int b = 0;
int result = a / b;
System.out.println(result);
}
}
这段代码可以正常编译。
但是运行时会抛出:
bash
java.lang.ArithmeticException: / by zero
因为整数不能除以 0。
这里编译器没有强制你写:
java
try {
int result = a / b;
} catch (ArithmeticException e) {
// 处理异常
}
原因是:在大量代码中,许多运行时问题理论上都可能发生。如果编译器要求每一次数组访问、方法调用、类型转换、整数运算都必须捕获异常,代码会变得非常臃肿。
2.2.3 常见 unchecked 异常逐个理解
1. NullPointerException
当变量为 null,却继续调用它的方法或访问它的属性时,会抛出该异常。
java
public class NullDemo {
public static void main(String[] args) {
String username = null;
System.out.println(username.length());
}
}
执行过程:
java
username = null
↓
调用 username.length()
↓
null 并不是一个真实对象
↓
抛出 NullPointerException
更好的处理方式通常不是到处捕获异常:
java
try {
System.out.println(username.length());
} catch (NullPointerException e) {
System.out.println("用户名不存在");
}
而是提前保证数据合法:
java
if (username != null) {
System.out.println(username.length());
}
或者在业务入口直接拒绝不合法参数:
java
import java.util.Objects;
public class UserService {
public void register(String username) {
Objects.requireNonNull(username, "用户名不能为空");
System.out.println("注册用户:" + username);
}
}
2. ArithmeticException
常见于整数除以 0。
java
public class AverageCalculator {
public static int average(int totalScore, int studentCount) {
if (studentCount <= 0) {
throw new IllegalArgumentException("学生人数必须大于 0");
}
return totalScore / studentCount;
}
}
这里没有等到程序真正发生 / by zero,而是提前校验参数。
3. NumberFormatException
当字符串无法转换成数字时,会抛出该异常。
java
public class AgeParser {
public static void main(String[] args) {
String input = "十八";
int age = Integer.parseInt(input);
System.out.println(age);
}
}
运行时会抛出 NumberFormatException,因为 "十八" 不是 Java 可以直接解析的整数格式。
但是,这里有一个重要细节:
unchecked 异常不一定永远代表程序员写错了代码。
如果字符串来自用户输入,那么用户输入错误本身就是正常业务情况,此时捕获 NumberFormatException 是合理的。
java
public class AgeParser {
public static void main(String[] args) {
String input = "十八";
try {
int age = Integer.parseInt(input);
System.out.println("年龄:" + age);
} catch (NumberFormatException e) {
System.out.println("请输入正确的数字年龄");
}
}
}
因此,不能机械地认为:
unchecked 异常 = 永远不能 catch
更准确的理解是:
unchecked 异常 = 编译器不强制处理
是否处理,取决于业务场景
4. IndexOutOfBoundsException
访问集合或数组中不存在的位置时,会抛出该异常。
java
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
List<String> names = List.of("小明", "小红");
System.out.println(names.get(2));
}
}
集合中只有:
下标 0 -> 小明
下标 1 -> 小红
但是代码访问了下标 2,因此程序运行时抛出异常。
正确做法通常是保证下标合法:
java
if (index >= 0 && index < names.size()) {
System.out.println(names.get(index));
}
5. IllegalArgumentException
当调用者传入不符合方法规则的参数时,通常可以主动抛出该异常。
例如,银行转账金额不能小于等于 0:
java
public class BankService {
public void transfer(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("转账金额必须大于 0");
}
System.out.println("成功转账:" + amount + " 元");
}
public static void main(String[] args) {
BankService service = new BankService();
service.transfer(-100);
}
}
这里的错误不是网络中断,也不是数据库临时不可用,而是调用者传入了明显不合法的参数。
所以使用 unchecked 异常比较合适。
三、try-catch-finally
3.1 try:放可能出问题的代码
try 块里放可能抛出异常的代码。
java
try {
int result = 10 / 0;
}
不要把大段无关代码都塞进 try。范围越大,越难判断真正出错的位置。
3.2 catch:捕获并处理异常
catch 用来处理异常。
java
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("除数不能为 0");
}
多个 catch 时,子类异常要写在前面,父类异常写在后面。
java
try {
// code
} catch (NumberFormatException e) {
// 更具体
} catch (RuntimeException e) {
// 更宽泛
}
如果把 RuntimeException 放前面,后面的 NumberFormatException 就永远匹配不到。
3.3 finally:无论成功失败都执行
finally 通常用于释放资源、清理现场。
java
public static List<String> tryCatchFinallyTrace(boolean throwException) {
List<String> trace = new ArrayList<>();
try {
trace.add("try");
if (throwException) {
throw new IllegalStateException("business failed");
}
trace.add("success");
} catch (IllegalStateException e) {
trace.add("catch");
} finally {
trace.add("finally");
}
return trace;
}
测试结果:
java
assertEquals(Arrays.asList("try", "catch", "finally"),
ExceptionKeywordDemo.tryCatchFinallyTrace(true));
assertEquals(Arrays.asList("try", "success", "finally"),
ExceptionKeywordDemo.tryCatchFinallyTrace(false));
不管是否发生异常,finally 都会执行。
3.4 不要在 finally 中 return
finally 里写 return 是非常危险的。
java
public static String badReturnInFinally() {
try {
return "try-return";
} finally {
return "finally-return";
}
}
最终返回的是:
text
finally-return
finally 的 return 会覆盖 try 里的 return,也可能吞掉原本的异常。项目中应当避免这种写法。
四、throw 和 throws
4.1 throw:方法体里主动抛异常
throw 用于主动抛出一个异常对象。
throw 后面必须跟一个可以被抛出的对象。Java 中只有 Throwable 或其子类对象能够被 throw 抛出,例如 Exception、RuntimeException、IOException 以及自定义异常对象。
假设系统要求用户年龄必须大于等于 18 岁。
java
public class UserService {
public static void validateAge(int age) {
if (age < 18) {
throw new InvalidAgeException("年龄必须大于等于 18 岁,当前年龄:" + age);
}
System.out.println("年龄校验通过");
}
}
自定义异常如下:
java
public class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message);
}
}
调用代码:
java
public class Application {
public static void main(String[] args) {
UserService.validateAge(16);
System.out.println("准备创建用户账号");
}
}
执行过程如下:
调用 validateAge(16)
↓
判断 age < 18,条件成立
↓
执行 throw new InvalidAgeException(...)
↓
当前方法立即终止
↓
异常向调用者传播
↓
后面的"准备创建用户账号"不会执行
运行结果类似:
Exception in thread "main" InvalidAgeException: 年龄必须大于等于 18 岁,当前年龄:16
这里最重要的是:
java
throw new InvalidAgeException(...);
并不是打印一个错误提示,也不是返回一个失败结果,而是:
立即中断当前正常执行流程,并抛出一个异常对象。
Java 语言规范将这种情况称为语句的"突然完成":throw 执行后,当前语句不会正常完成,而是立即发生控制流转移,直到找到能够捕获该异常的 catch;如果始终没有找到,当前线程会因未捕获异常而终止。
4.2 throws:方法签名上声明异常
throws 用于告诉调用者:这个方法可能抛出某些异常。
有些方法只负责完成底层任务,并不知道失败后应该如何提示用户。
例如,一个工具方法只负责读取文件:
java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class FileService {
public static String readUserFile(String fileName) throws IOException {
return Files.readString(Path.of(fileName));
}
}
这里的 throws IOException 表示:
我这个方法可能读取失败,但我不知道失败后应该弹窗、重试、退出程序还是记录日志,所以交给调用我的代码决定。
调用者可以在更合适的位置处理:
java
import java.io.IOException;
public class Application {
public static void main(String[] args) {
try {
String content = FileService.readUserFile("user.txt");
System.out.println(content);
} catch (IOException e) {
System.out.println("文件读取失败,请检查文件是否存在");
}
}
}
在 Java 中,checked 异常会成为方法契约的一部分:调用者看到方法声明中的 throws IOException,就知道调用这个方法时必须考虑读取失败的情况。
所以throw它不是处理异常,而是把异常处理责任交给调用方。
throw 和 throws 的区别:
throw在方法体内,后面跟异常对象。throws在方法声明上,后面跟异常类型。throw是真的把异常抛出去。throws是声明可能会抛。
throw:抛出异常,使正常流程中断。
catch:捕获并处理异常。
throws:声明该方法可能把异常继续交给调用者。
五、自定义异常和异常链
异常不仅仅是"程序出错了"的提示,它还承担着两个重要任务:
- 告诉调用者:发生了什么业务问题。
- 告诉开发者:这个问题最初是由什么底层原因引起的。
如果项目中所有异常都直接写成:
java
throw new RuntimeException("出错了");
那么程序虽然能够中断执行,但异常表达的信息非常模糊。
例如:注册失败了
到底是:
- 年龄不合法?
- 用户名重复?
- 数据库连接失败?
- 文件写入失败?
- 网络请求超时?
调用者无法根据异常类型做出准确处理,开发者排查问题时也很困难。
因此,实际项目中通常需要使用:
- 自定义异常:表达业务语义
- 异常链:保留底层原因
5.1 为什么需要自定义异常
5.1.1 直接抛出 RuntimeException 的问题
假设现在有一个用户注册功能,要求年龄必须在 0 ~ 150 之间。
最简单的写法如下:
java
public class UserService {
public void register(String username, int age) {
if (age < 0 || age > 150) {
throw new RuntimeException("年龄不合法");
}
System.out.println("注册成功:" + username);
}
}
这段代码虽然可以工作,但存在一个问题:
java
throw new RuntimeException("年龄不合法");
RuntimeException 只能说明程序运行时出现了问题,却无法从异常类型上直接看出这是一个"年龄校验失败"的业务问题。
调用方看到的只是:
java
try {
userService.register("小明", -10);
} catch (RuntimeException e) {
System.out.println("注册失败");
}
这里的 catch (RuntimeException e) 范围过大。
因为它可能捕获到:
年龄不合法
用户名为空
数据库保存失败
空指针异常
数组越界异常
程序状态错误
也就是说,所有错误都被装进了同一个模糊的袋子里。
可以把它理解为:
医院只给所有病人开一张诊断单,上面统一写着"身体不舒服"。虽然没有说错,但完全无法指导下一步治疗。
5.1.2 自定义异常让错误具有业务名字
更好的方式是定义一个专门表示年龄不合法的异常:
java
public class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message);
}
}
然后在业务代码中使用它:
java
public class UserService {
public void register(String username, int age) {
if (age < 0 || age > 150) {
throw new InvalidAgeException("年龄必须在 0 到 150 之间,当前值:" + age);
}
System.out.println("注册成功:" + username);
}
}
调用代码如下:
java
public class Application {
public static void main(String[] args) {
UserService userService = new UserService();
try {
userService.register("小明", -10);
} catch (InvalidAgeException e) {
System.out.println("注册失败:" + e.getMessage());
}
}
}
运行结果:
注册失败:年龄必须在 0 到 150 之间,当前值:-10
现在异常类型本身就具有明确语义:调用者立刻知道问题发生在年龄校验阶段
这就是自定义异常最重要的价值:
把模糊的技术错误,转换成清晰的业务语言。
5.1.3 自定义异常不是为了"多写一个类"
初学者可能会觉得:
使用
RuntimeException已经可以抛异常了,为什么还要额外写一个异常类?
因为在真实项目中,异常不仅给开发者看,也会影响程序后续处理逻辑。
例如,用户注册时可能发生三种不同问题:
java
public class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message);
}
}
public class UsernameAlreadyExistsException extends RuntimeException {
public UsernameAlreadyExistsException(String message) {
super(message);
}
}
public class UserSaveException extends RuntimeException {
public UserSaveException(String message, Throwable cause) {
super(message, cause);
}
}
业务代码:
java
public class UserService {
public void register(String username, int age) {
if (age < 0 || age > 150) {
throw new InvalidAgeException("年龄不合法:" + age);
}
if ("admin".equals(username)) {
throw new UsernameAlreadyExistsException("用户名已存在:" + username);
}
System.out.println("注册成功:" + username);
}
}
调用方就可以针对不同错误给出不同提示:
java
public class Application {
public static void main(String[] args) {
UserService userService = new UserService();
try {
userService.register("admin", 18);
} catch (InvalidAgeException e) {
System.out.println("请输入正确年龄");
} catch (UsernameAlreadyExistsException e) {
System.out.println("该用户名已被占用,请更换用户名");
} catch (UserSaveException e) {
System.out.println("系统繁忙,请稍后重试");
}
}
}
执行逻辑如下:
用户注册
↓
年龄错误 ─────────→ 提示重新填写年龄
↓
用户名重复 ───────→ 提示更换用户名
↓
保存失败 ─────────→ 提示稍后重试
如果所有异常都写成 RuntimeException,调用方就很难准确区分这些情况。
5.2 自定义异常的基本写法
5.2.1 继承 RuntimeException:定义 unchecked 业务异常
例如,年龄非法通常属于参数或业务规则不合法,可以定义为 unchecked 异常:
java
public class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message);
}
}
因为它继承自 RuntimeException,所以调用方不需要被编译器强制捕获。RuntimeException 本身继承自 Exception,它及其子类属于 unchecked 异常。
使用方式:
java
public class UserService {
public void updateAge(int age) {
if (age < 0 || age > 150) {
throw new InvalidAgeException("年龄必须在 0 到 150 之间");
}
System.out.println("年龄修改成功:" + age);
}
}
调用者可以选择捕获:
java
try {
userService.updateAge(-1);
} catch (InvalidAgeException e) {
System.out.println(e.getMessage());
}
也可以不捕获,让它继续向上传播,交给项目中的统一异常处理机制处理。
5.2.2 继承 Exception:定义 checked 业务异常
如果某个异常表示调用者必须明确处理的失败场景,也可以继承 Exception。
例如,系统导入用户数据文件失败后,调用者可能需要提示用户重新上传文件:
java
public class UserImportException extends Exception {
public UserImportException(String message) {
super(message);
}
}
业务代码:
java
public class UserImportService {
public void importUserFile(String filePath) throws UserImportException {
if (filePath == null || filePath.isBlank()) {
throw new UserImportException("导入文件路径不能为空");
}
System.out.println("开始导入文件:" + filePath);
}
}
调用方必须处理:
java
public class Application {
public static void main(String[] args) {
UserImportService service = new UserImportService();
try {
service.importUserFile("");
} catch (UserImportException e) {
System.out.println("导入失败:" + e.getMessage());
}
}
}
5.3 什么是异常包装
在真实项目中,代码通常会分层。
例如,一个配置读取功能可能分为:
Application 层:负责启动程序、展示最终提示
↓
ConfigService 层:负责配置业务逻辑
↓
File 操作层:负责真正读取文件
底层读取文件时,可能抛出:IOException;
但是对于上层业务来说,它真正关心的不是:某个字节流读取失败
而是:系统配置加载失败
因此,业务层通常会捕获底层异常,再抛出一个更符合当前业务含义的新异常:
java
try {
Files.readString(Path.of("application.properties"));
} catch (IOException e) {
throw new ConfigLoadException("配置文件加载失败", e);
}
这个过程称为:异常包装
也可以理解为:将底层技术异常转换成上层能够理解的业务异常
例如:
IOException
↓ 包装
ConfigLoadException
↓ 上层看到
系统配置加载失败
异常包装的目的不是隐藏错误,而是让不同层看到适合自己的异常语义。
5.4 异常链到底长什么样
假设现在有如下异常链:
ConfigLoadException
↓ caused by
IOException
↓ caused by
NoSuchFileException
它表达的含义是:
程序配置加载失败,
是因为文件读取失败,
而文件读取失败的根本原因是文件不存在。
程序打印堆栈信息时,通常会看到类似内容:
Exception in thread "main" ConfigLoadException: 配置加载失败
at ConfigService.loadConfig(ConfigService.java:20)
at Application.main(Application.java:8)
Caused by: java.nio.file.NoSuchFileException: application.properties
at java.base/sun.nio.fs.WindowsException.translateToIOException(...)
at java.base/java.nio.file.Files.readString(...)
at ConfigService.loadConfig(ConfigService.java:18)
这里最关键的部分是:
Caused by: java.nio.file.NoSuchFileException: application.properties
它告诉开发者:
业务层看到的是"配置加载失败",
真正需要修复的是"配置文件不存在"。
因此,阅读异常堆栈时,不应该只看最上面的异常名称,还要继续寻找:Caused by:
直到找到最底层的根本原因。
六、try-with-resources
6.1 为什么需要 try-with-resources
文件流、网络连接、数据库连接这类资源,用完必须关闭。
传统写法通常是:
java
TestResource resource = new TestResource(false, false);
try {
return resource.read();
} finally {
resource.close();
}
这种写法能关闭资源,但当 read() 和 close() 都抛异常时,原始异常可能被 close() 的异常覆盖。
Java 语言规范规定:普通 try-finally 中,如果 try 因异常而突然结束,随后 finally 又因新的异常而突然结束,最终以 finally 的异常为准,原先 try 中的异常原因会被丢弃。
6.2 try-with-resources 的写法
Java 7 引入了 try-with-resources。
java
public static String useTryWithResources(boolean readFailed, boolean closeFailed) throws IOException {
try (TestResource resource = new TestResource(readFailed, closeFailed)) {
return resource.read();
}
}
只要资源实现了 AutoCloseable 或 Closeable,就可以放进 try (...) 里。代码块结束后,资源会自动关闭。
6.3 suppressed exception
如果 read() 抛出异常,close() 也抛出异常,try-with-resources 会保留主异常,并把关闭资源时的异常放进 suppressed 列表。
测试代码:
java
try {
ExceptionKeywordDemo.useTryWithResources(true, true);
fail("Expected IOException");
} catch (IOException e) {
assertEquals("read failed", e.getMessage());
assertEquals(1, e.getSuppressed().length);
assertEquals("close failed", e.getSuppressed()[0].getMessage());
}
这比手写 finally 更可靠,因为它不会把真正的业务异常弄丢。
七、项目和面试如何考察
7.1 项目中常见用法
项目中异常处理通常出现在这些位置:
- Controller:统一把异常转换成 HTTP 响应。
- Service:抛出业务异常,如库存不足、订单状态非法。
DAO:处理数据库异常,必要时转换为数据访问异常。- Job:捕获任务异常,记录日志,避免调度线程直接中断。
- 消息消费:单条消息失败时决定重试、丢弃还是进死信队列。
一个成熟项目通常不会到处散落 try-catch,而是有统一异常处理机制。
7.2 面试高频问题
常见问题:
Exception和Error的区别?- checked 异常和 unchecked 异常的区别?
throw和throws的区别?finally一定会执行吗?finally中 return 会发生什么?try-with-resources的原理是什么?- 什么是 suppressed exception?
- 为什么不建议捕获
Throwable? - 如何避免
NullPointerException? try-catch会影响性能吗?
7.3 面试回答思路
回答异常问题时,不要只背概念。可以按这条线说:
text
异常体系 -> 编译期约束 -> 运行时处理 -> 项目实践 -> 底层原理
比如回答 try-with-resources:
- 它用于自动关闭资源。
- 资源必须实现
AutoCloseable。 - 编译器会把它转换成类似
try-finally的结构。 - 如果业务异常和关闭异常同时发生,关闭异常会进入 suppressed 列表。
- 项目中处理
IO、JDBC、流对象时优先使用它。
这样回答比只说"它能自动关闭资源"更有深度。
总结
Java 异常处理的核心,是在程序出现问题时及时中断错误流程,并保留清晰的排查线索。
本文主要介绍了以下内容:
checked异常要求显式处理,常用于文件、数据库等外部风险。unchecked异常通常表示参数非法或代码逻辑问题。try-catch-finally用于捕获异常和清理资源。throw表示真正抛出异常,throws表示声明异常风险。- 自定义异常能够让业务含义更加清晰,异常链能够保留原始原因。
try-with-resources可以自动关闭资源,并避免关闭异常覆盖主异常。