try-catch-finally 执行顺序:一个细节决定成败
写 Java 代码这么多年,try-catch-finally 你一定用过无数次。但我敢打赌,你一定没注意过以下几个细节:
- finally 里的 return 会怎样?
- try 里的 return 和 finally 谁先执行?
- 如果 catch 和 finally 都抛出异常,最终是哪个?
- try-with-resources 是什么,它怎么保证资源关闭?
这些问题看起来简单,但真正答对的人不多。面试中被问到这类题目的候选人,一半以上会答错。今天我们就来把这个执行顺序彻底讲透。
一、真实面试场景
候选人小刘在面试某大厂时,被问到这样一个问题:
public class Test {
public static int getValue() {
int x = 1;
try {
return x;
} finally {
x = 2;
}
}
public static void main(String[] args) {
System.out.println(getValue());
}
}
面试官问:"输出什么?"
小刘说:"2,因为 finally 会执行。"
面试官摇摇头:"你跑一下看看。"
小刘跑了一下,发现输出是 1,而不是 2。
小刘愣住了。
【面试官心理】
这道题考察的是候选人是否理解 try-catch-finally 的执行顺序。finally 确实会执行,但它执行在 return 之前——return 的值已经被保存了,finally 修改的是局部变量,不是返回值。只有理解了执行顺序,才能答对这个问题。
二、执行顺序基础
2.1 标准执行顺序
try {
// 1. 首先执行这里
System.out.println("try");
int result = 10 / 0; // 抛出 ArithmeticException
} catch (ArithmeticException e) {
// 2. 如果捕获到异常,执行这里
System.out.println("catch");
} finally {
// 3. 最后执行这里(无论是否捕获异常)
System.out.println("finally");
}
标准输出顺序:try -> catch -> finally
2.2 没有异常的执行顺序
try {
System.out.println("try");
} catch (Exception e) {
System.out.println("catch"); // 不执行
} finally {
System.out.println("finally"); // 仍然执行
}
输出:try -> finally
2.3 多个 catch 的执行顺序
try {
int[] arr = null;
arr[0] = 1; // 抛出 NullPointerException
} catch (ArithmeticException e) {
System.out.println("算术异常"); // 不匹配
} catch (NullPointerException e) {
System.out.println("空指针异常"); // 匹配,执行这个
} catch (Exception e) {
System.out.println("其他异常"); // 不匹配,不会执行
} finally {
System.out.println("finally");
}
注意:catch 的顺序很重要!子类异常要放在父类异常前面。
三、return 与 finally 的执行顺序(重点!)
3.1 return 的执行时机
public static int getValue() {
int x = 1;
try {
return x; // 1. 保存返回值 x 的值(1)
} finally {
x = 2; // 2. finally 执行,修改 x
}
// 3. 返回保存的值(1)
}
执行顺序:
- 执行
return x,但不是立即返回,而是先把返回值保存起来
- 执行
finally 中的代码
- 返回之前保存的值
所以输出是 1,而不是 2。
3.2 ❌ 错误理解:finally 修改会影响返回值
// 常见错误理解
public static int test() {
int x = 1;
try {
return x; // 以为这里会直接返回
} finally {
x = 2; // 以为会返回修改后的值
}
}
正确理解:return x 会先把 x 的当前值保存起来,然后执行 finally,最后返回保存的值。finally 修改 x 不会影响已经保存的返回值。
3.3 【直观类比】快照机制
想象你在拍照:
return x 就是拍一张快照,拍了 x=1 的照片
finally 修改 x 就像事后化妆,但照片已经拍好了,不会变
int x = 1;
int photo = x; // 拍快照:photo = 1
x = 2; // 修改 x:x = 2
return photo; // 返回快照:返回 1
3.4 finally 里的 return(坑王!)
public static int getValue() {
int x = 1;
try {
return x;
} finally {
return 100; // finally 里有 return,会覆盖 try 的 return
}
}
输出:100
public static int getValue() {
int x = 1;
try {
int y = x / 0; // 抛异常
return x;
} catch (Exception e) {
return 2;
} finally {
return 3; // finally 里有 return,会覆盖 catch 的 return
}
}
输出:3
⚠️
面试常考:finally 里的 return 会覆盖 try/catch 中的 return。这是最容易被忽略的坑。如果 finally 中有 return,finally 块之后的所有代码都不会执行(因为已经返回了)。
四、异常传播与覆盖
4.1 finally 抛出异常会覆盖 try/catch 的异常
public static void test() {
try {
throw new RuntimeException("try exception");
} catch (RuntimeException e) {
System.out.println("catch: " + e.getMessage());
throw e; // 重新抛出
} finally {
throw new RuntimeException("finally exception"); // 这个会覆盖 catch 的异常
}
}
运行时会发现:catch 块里的 throw e; 被 finally 的异常覆盖了,最终抛出的是 "finally exception"。
4.2 JDK 7 之前:异常被覆盖的问题
try {
conn.execute(sql);
} finally {
conn.close(); // 如果这里抛异常,sql 的异常就没了
}
这就是"异常屏蔽"问题。JDK 7 的 try-with-resources 解决了这个问题。
4.3 JDK 7+:try-with-resources 解决这个问题
try (Connection conn = DriverManager.getConnection(url)) {
conn.execute(sql);
} // 自动关闭,即使关闭时抛异常,也会作为"被抑制的异常"附加到主异常上
被抑制的异常可以通过 Throwable.getSuppressed() 获取:
try {
// 可能抛出异常的操作
} catch (Exception e) {
Exception[] suppressed = e.getSuppressed();
for (Exception s : suppressed) {
System.out.println("被抑制的异常: " + s.getMessage());
}
}
五、实际代码场景
5.1 ❌ 错误示范:在 finally 里释放空资源
Connection conn = null;
try {
conn = DriverManager.getConnection(url);
conn.execute(sql);
} finally {
if (conn != null) {
conn.close(); // 如果上面已经抛异常,这里可能还没赋值
}
}
这个写法有个问题:如果 DriverManager.getConnection(url) 抛异常,conn 还是 null,finally 里的 if (conn != null) 能正确处理。但如果不是这种情况,问题更复杂。
5.2 ✅ 正确示范:try-with-resources
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, param);
ps.executeUpdate();
} // 自动关闭资源,无论是否正常结束或抛异常
JDK 7+ 的 try-with-resources 特点:
- 资源在 try 语句结束时自动关闭
- 关闭时抛出的异常会作为"被抑制异常"附加到主异常上
- 资源必须实现
AutoCloseable 接口
5.3 实现 AutoCloseable
public class Resource implements AutoCloseable {
private String name;
public Resource(String name) {
this.name = name;
System.out.println("创建资源: " + name);
}
@Override
public void close() throws Exception {
System.out.println("关闭资源: " + name);
}
}
// 使用
try (Resource r = new Resource("DB")) {
System.out.println("使用资源");
} // 自动调用 close()
5.4 多个资源的关闭顺序
try (Resource r1 = new Resource("A");
Resource r2 = new Resource("B")) {
// 先创建的先获得资源
} // 后创建的先关闭(B 先关闭,然后 A)
六、执行顺序总结
七、生产场景与避坑
7.1 ❌ 错误示范:finally 里有 return
public String getConfig(String key) {
try {
return configMap.get(key);
} finally {
return "default"; // 永远返回 default,try 的 return 被覆盖
}
}
7.2 ❌ 错误示范:在 finally 里抛异常
try {
doSomething();
} finally {
closeConnection(); // 如果 closeConnection 抛异常,会覆盖 doSomething 的异常
}
7.3 ✅ 正确做法:不用 return,让流程自然结束
public int getValue() {
int x = 1;
try {
return x;
} finally {
x = 2; // 修改 x,但不影响返回值
}
}
public String getConfig(String key) {
String value = null;
try {
value = configMap.get(key);
} finally {
// 不要在这里 return
}
return value; // 在 finally 之后 return
}
7.4 ✅ 正确做法:使用 suppressed exceptions(JDK 7+)
public void process() throws Exception {
try {
throw new RuntimeException("主异常");
} finally {
try {
close(); // 关闭可能失败
} catch (Exception e) {
// JDK 7+ 可以将关闭异常作为被抑制异常
}
}
}
八、面试追问链
第一层:基础执行顺序
面试官问:"try-catch-finally 的执行顺序是什么?"
标准回答:先执行 try,如果 try 正常结束就执行 finally;如果 try 抛异常且被 catch 捕获,就执行 catch 再执行 finally;如果异常没被 catch,就执行 finally 后异常继续传播。
第二层:return 与 finally
面试官追问:"try 里的 return 和 finally 谁先执行?"
标准回答:return 不是立即返回,而是先保存返回值,然后执行 finally,最后才返回。如果 finally 里也 return,会覆盖 try 的 return。
第三层:异常覆盖
面试官追问:"如果 catch 和 finally 都抛出异常,最终是哪个?"
标准回答:finally 的异常会覆盖 catch 的异常。如果没有 finally return,catch 的异常会被 finally 的异常覆盖;如果有 finally return,finally 的 return 值会覆盖一切。
第四层:try-with-resources
面试官追问:"try-with-resources 是什么?它怎么保证资源关闭?"
标准回答:实现了 AutoCloseable 接口的资源可以在 try 语句中声明,try 结束时自动调用 close(),即使关闭时抛异常也会作为被抑制异常附加到主异常上。
【面试官心理】
这道题考察的是候选人对异常处理细节的理解。知道执行顺序只是基础,能解释 return 的保存机制、异常覆盖问题、try-with-resources 的设计,才是真正的深度理解。
【学习小结】
- finally 总是执行(即使 try/catch 有 return)
- return 先保存值,再执行 finally,最后返回保存的值
- finally 里的 return 会覆盖 try/catch 的 return
- finally 抛异常会覆盖 try/catch 的异常
- try-with-resources 自动关闭资源,保留完整的异常链
- 最佳实践:避免在 finally 里 return 或抛异常