大家好,我是小富。
《十万个why》系列持续更新中
面试里经常会考 try-catch-finally 的执行顺序,大多数人背得滚瓜烂熟:"finally 一定会执行"。
但如果再追问一层:如果 try 里有 return,finally 里也有 return,最终返回哪个?如果 try 里抛了异常,finally 里也有 return,异常还能抛出去吗?
很多人就开始迟疑了。而这两个问题的答案,反直觉到可能让你怀疑 Java 的设计。
第一个反直觉:finally 的 return 会覆盖 try 的 return
public static int test() {
try {
return 1;
} finally {
return 2;
}
}
public static void main(String[] args) {
System.out.println(test()); // 输出什么?
}
输出是 2,不是 1。
try 块里明明已经 return 1 了,但 finally 块的 return 2 把它覆盖了。
这是因为 JVM 在字节码层面的处理方式:当 try 块执行到 return 时,JVM 会先把返回值暂存起来,然后去执行 finally 块。如果 finally 块里有自己的 return,方法就直接从 finally 返回了,try 里暂存的那个返回值被丢弃。
看字节码就更清楚了:
// 简化版字节码
0: iconst_1 // 压入 1
1: istore_0 // 暂存到局部变量表(准备 return)
2: iconst_2 // finally 块:压入 2
3: ireturn // 直接返回 2,不再回到 try 的 return
第二个反直觉:finally 的 return 会吞掉异常
这个比前一个更危险:
public static int test() {
try {
int a = 1 / 0; // 会抛 ArithmeticException
return 1;
} catch (Exception e) {
throw new RuntimeException("出错了", e);
} finally {
return 3;
}
}
public static void main(String[] args) {
System.out.println(test()); // 输出什么?会抛异常吗?
}
答案是:输出 3,不会抛异常。
catch 块明明 throw 了一个 RuntimeException,但 finally 的 return 把这个异常"吞掉"了。程序像什么都没发生一样正常返回了 3。
这意味着如果你在 finally 里写了 return,try/catch 中的任何异常都不会往外抛。异常信息丢失了,日志里没有,监控上看不到,程序还在正常运行——就像一个沉默的定时炸弹。
第三个反直觉:finally 修改变量不影响 try 的返回值(基本类型)
public static int test() {
int x = 1;
try {
return x;
} finally {
x = 99;
}
}
public static void main(String[] args) {
System.out.println(test()); // 输出什么?
}
输出是 1,不是 99。
前面不是说 finally 一定执行吗?确实,finally 执行了,x 也确实被改成了 99。但 try 里 return x 执行时,JVM 已经把 x 的值(1)拷贝到了操作数栈的临时位置。finally 修改的是局部变量 x,不是那个已经被暂存的返回值副本。
但如果是引用类型,情况就不同了:
public static List<String> test() {
List<String> list = new ArrayList<>();
list.add("a");
try {
return list;
} finally {
list.add("b"); // 修改的是对象本身,不是引用
}
}
public static void main(String[] args) {
System.out.println(test()); // 输出 [a, b]
}
输出是 [a, b]。因为 try 暂存的是 list 的引用(指向堆上的对象),finally 通过同一个引用修改了对象的内容,所以调用方拿到的对象是被修改过的。
为什么 Java 要这样设计?
从 JVM 规范的角度看,这个行为是字节码编译方式的直接结果。Java 编译器在处理 finally 块时,会在每一个可能的退出路径(正常 return、异常 throw)之前都插入 finally 的代码。如果 finally 本身有 return,它就成了最终的退出点。
这不是 bug,是规范行为。但它确实违反了很多人的直觉,以至于各种编码规范都会明确禁止:
阿里巴巴 Java 开发手册:不要在 finally 块中使用 return。
SonarQube 规则:S1143 - Jump statements should not occur in "finally" blocks
Checkstyle 规则:IllegalToken - 禁止在 finally 中使用 return
实际翻车案例
public boolean transferMoney(Long from, Long to, BigDecimal amount) {
try {
accountService.deduct(from, amount);
accountService.add(to, amount);
return true;
} catch (Exception e) {
log.error("转账失败", e);
throw e; // 想把异常往上抛
} finally {
// 某个开发同学加了个资源清理,顺手写了个 return
cleanupResources();
return false; // 灾难!
}
}
这段代码的问题:
- 转账成功了,return true 被 finally 的 return false 覆盖,调用方以为转账失败了
- 转账失败了,catch 里 throw 的异常被 finally 的 return false 吞掉了,上层完全不知道出了异常
- 不管成功还是失败,永远返回 false
正确姿势
// finally 只做资源清理,不要有 return
public boolean transferMoney(Long from, Long to, BigDecimal amount) {
try {
accountService.deduct(from, amount);
accountService.add(to, amount);
return true;
} catch (Exception e) {
log.error("转账失败", e);
throw e;
} finally {
cleanupResources(); // 只清理资源,不 return
}
}
// 更好的选择:用 try-with-resources 替代手动 finally
try (Connection conn = dataSource.getConnection()) {
// 使用 conn
} // 自动 close,不需要 finally
总结
| 场景 | 行为 | 危险程度 |
|---|---|---|
| try 有 return,finally 也有 return | finally 的 return 覆盖 try 的 | 高:返回值被篡改 |
| try/catch 抛异常,finally 有 return | 异常被吞掉,不会往外抛 | 极高:静默丢失异常 |
| try 有 return,finally 修改基本类型变量 | 不影响返回值(值已被暂存) | 低:但容易造成困惑 |
| try 有 return,finally 修改引用类型对象 | 影响返回值(修改了对象内容) | 中:可能导致数据意外变更 |
一条铁律:永远不要在 finally 块中写 return。 finally 只做清理工作——关闭连接、释放锁、清理临时文件。任何业务逻辑和返回值都不应该出现在 finally 里。
我是小富,下期见。
