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)
}

执行顺序:

  1. 执行 return x,但不是立即返回,而是先把返回值保存起来
  2. 执行 finally 中的代码
  3. 返回之前保存的值

所以输出是 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 特点:

  1. 资源在 try 语句结束时自动关闭
  2. 关闭时抛出的异常会作为"被抑制异常"附加到主异常上
  3. 资源必须实现 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)

六、执行顺序总结

场景执行顺序结果
try 正常,无异常try -> finallyfinally 总是执行
try 抛异常,catch 匹配try -> catch -> finallycatch 处理异常
try 抛异常,catch 不匹配try -> finally(异常传播)异常继续往上抛
try return,finally 修改变量try 保存值 -> finally -> return返回保存的值
try return,finally 也有 returntry 保存值 -> finally return返回 finally 的值
finally 抛异常覆盖 catchcatch -> finally throw最终抛出 finally 的异常
try-with-resources自动关闭资源,抑制异常附加到主异常异常链完整保留

七、生产场景与避坑

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 或抛异常