跳至主要內容

程序员小富大约 4 分钟

大家好,我是小富。

《十万个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; // 灾难!
    }
}

这段代码的问题:

  1. 转账成功了,return true 被 finally 的 return false 覆盖,调用方以为转账失败了
  2. 转账失败了,catch 里 throw 的异常被 finally 的 return false 吞掉了,上层完全不知道出了异常
  3. 不管成功还是失败,永远返回 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 也有 returnfinally 的 return 覆盖 try 的高:返回值被篡改
try/catch 抛异常,finally 有 return异常被吞掉,不会往外抛极高:静默丢失异常
try 有 return,finally 修改基本类型变量不影响返回值(值已被暂存)低:但容易造成困惑
try 有 return,finally 修改引用类型对象影响返回值(修改了对象内容)中:可能导致数据意外变更

一条铁律:永远不要在 finally 块中写 return。 finally 只做清理工作——关闭连接、释放锁、清理临时文件。任何业务逻辑和返回值都不应该出现在 finally 里。


我是小富,下期见。

上次编辑于: