finally 与 try-with-resources
本教程共 100 篇 · 第 67 篇 · 更新于 2026-08-05 · 约 18 分钟阅读
67. finally 与 try-with-resources
本节目标:弄清 finally 的执行时机和它的几个坑,学会用 try-with-resources 自动关闭资源,从此不再手写一堆 close。
finally:无论如何都要跑的那段
有些代码不管成功失败都必须执行,比如关闭文件、释放锁、打印结束标记。
只写在 try 里不行——出异常就跳过了。每个 catch 里抄一遍也不行——代码重复还容易漏。
finally 就是为这个场景准备的。
public class FinallyBasic {
public static void main(String[] args) {
try {
System.out.println("try 开始");
int x = 1 / 0;
System.out.println("这行不会执行");
} catch (ArithmeticException e) {
System.out.println("catch 处理");
} finally {
System.out.println("finally 一定执行");
}
System.out.println("后续代码");
}
}
```text
输出:
```text
try 开始
catch 处理
finally 一定执行
后续代码
把 1 / 0 改成 1 / 1,输出会变成 try 开始 → 这行不会执行(其实会执行)→ finally 一定执行 → 后续代码。
无论走不走 catch,finally 都在最后跑一遍。
finally 的执行时机
finally 的执行点比想象中更「顽固」:
try正常结束 → 执行finallytry抛异常且被catch接住 → 执行catch,再执行finallytry抛异常但没人接 → 先执行finally,再把异常往上抛try或catch里有return→ 先执行finally,再真正返回
最后一条最反直觉。看这个例子:
public class FinallyWithReturn {
public static void main(String[] args) {
System.out.println(test());
}
static String test() {
try {
System.out.println("try 里");
return "从 try 返回";
} finally {
System.out.println("finally 照样执行");
}
}
}
```text
输出:
```text
try 里
finally 照样执行
从 try 返回
return 语句准备好返回值后,会被「暂停」,等 finally 跑完才真正返回。
Note
finally也有失效的时候:如果try块里调用了System.exit(0)直接终止 JVM,或者进程被强杀,finally就没机会执行了。这属于极端情况,正常写业务代码不用担心。
陷阱一:finally 里 return 会覆盖返回值
这是面试里出镜率很高的题。
public class FinallyOverride {
public static void main(String[] args) {
System.out.println(test()); // 输出什么?
}
static int test() {
try {
return 1;
} finally {
return 2; // 危险写法
}
}
}
```java
答案是 `2`。
`try` 里的 `return 1` 被 `finally` 里的 `return 2` 直接顶掉了。更糟的是,如果 `try` 里抛了异常,`finally` 里的 `return` 还会把**异常也一起吞掉**。
```java
static int swallow() {
try {
throw new RuntimeException("重要错误");
} finally {
return -1; // 异常凭空消失
}
}
调用 swallow() 得到 -1,那个 RuntimeException 连影子都看不到。
Warning永远不要在
finally里写return,也不要在finally里throw异常。finally只做清理,不做流程控制。主流 IDE 和静态检查工具都会对这种写法报警告。
陷阱二:异常屏蔽
如果 catch 正准备抛异常,finally 里又抛了另一个异常,会发生什么?
public class SuppressDemo {
public static void main(String[] args) {
try {
Integer.parseInt("abc");
} catch (Exception e) {
throw new RuntimeException("包装后的异常", e);
} finally {
throw new IllegalStateException("finally 里的异常");
}
}
}
```bash
运行只会看到 `IllegalStateException`。`catch` 里那个 `RuntimeException` 消失了。
原因很简单:**一个方法同一时刻只能抛出一个异常**。`finally` 后发生、后抛出,就把前面的顶掉了。被顶掉的那个叫「被屏蔽的异常」(Suppressed Exception)。
这是老式 `finally` 写法的先天缺陷。而 `try-with-resources` 恰好解决了它。
## 老式资源关闭有多啰嗦
Java 7 之前,关闭文件流是这个画风:
```java
// 历史兼容:Java 7 之前的写法,现在别这么写
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class OldStyleClose {
static String readFirstLine(String path) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
return br.readLine();
} finally {
if (br != null) {
br.close(); // close 自己还可能抛 IOException
}
}
}
}
问题一大堆:变量要提到外面声明、要判 null、close() 自己还会抛异常。
如果同时开两个资源,还得嵌套两层 try-finally,代码瞬间变成金字塔。更要命的是,内层 close() 抛异常会导致外层资源泄漏——外层根本没机会关。
try-with-resources:让 JVM 帮你关
Java 7 引入了 try-with-resources(也叫 ARM,自动资源管理)。把资源声明写在 try 后面的圆括号里,就这么简单:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResources {
public static void main(String[] args) {
try {
System.out.println(readFirstLine("demo.txt"));
} catch (IOException e) {
System.out.println("读取失败:" + e.getMessage());
}
}
// 资源写在 try 后的括号里,块结束自动 close
static String readFirstLine(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
}
```bash
没有 `finally`,没有判 `null`,没有手写 `close()`。
不管 `try` 块是正常结束还是抛异常退出,JVM **保证**会调用资源的 `close()` 方法。
## 声明多个资源
多个资源用分号隔开,写在同一个括号里:
```java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class MultiResource {
static void copyFirstLine(String src, String dest) throws IOException {
try (BufferedReader in = new BufferedReader(new FileReader(src));
BufferedWriter out = new BufferedWriter(new FileWriter(dest))) {
String line = in.readLine();
if (line != null) {
out.write(line);
}
}
}
}
关闭顺序是创建顺序的逆序:先关 out,再关 in。这跟先开的后关的直觉一致,也符合资源依赖关系。
AutoCloseable:入场券
不是随便什么对象都能放进 try(...),它必须实现 java.lang.AutoCloseable 接口。
这个接口只有一个方法:
public interface AutoCloseable {
void close() throws Exception;
}
```java
JDK 里的 IO 流、`Scanner`、`ZipFile`、JDBC 的 `Connection` 和 `Statement`,全都实现了它,直接用就行。
你自己的类也可以实现:
```java
public class MyResource implements AutoCloseable {
private final String name;
public MyResource(String name) {
this.name = name;
System.out.println(name + " 打开了");
}
public void doWork() {
System.out.println(name + " 干活中");
}
@Override
public void close() {
System.out.println(name + " 关闭了");
}
public static void main(String[] args) {
try (MyResource a = new MyResource("资源A");
MyResource b = new MyResource("资源B")) {
a.doWork();
b.doWork();
}
}
}
输出:
资源A 打开了
资源B 打开了
资源A 干活中
资源B 干活中
资源B 关闭了
资源A 关闭了
```bash
逆序关闭看得清清楚楚。
> [!TIP]
> `java.io.Closeable` 是 `AutoCloseable` 的子接口,区别只在 `close()` 声明抛出的异常类型:`Closeable` 抛 `IOException`,`AutoCloseable` 抛 `Exception`。自己写类时优先实现 `AutoCloseable`。
## 被压制的异常去哪了
`try-with-resources` 对异常屏蔽的处理,比老式 `finally` 高明得多。
假设 `try` 块抛了异常,随后 `close()` 也抛了异常。老式写法里前者会被后者顶掉,而 `try-with-resources` **保留 try 块的异常作为主异常**,把 `close()` 的异常「压制」起来挂在主异常上。
```java
public class SuppressedDemo {
static class BadResource implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("关闭时出错");
}
}
public static void main(String[] args) {
try (BadResource r = new BadResource()) {
throw new RuntimeException("干活时出错");
} catch (Exception e) {
System.out.println("主异常:" + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println("被压制的异常:" + s.getMessage());
}
}
}
}
输出:
主异常:干活时出错
被压制的异常:关闭时出错
```bash
两个异常一个都没丢。`getSuppressed()` 返回一个数组,装着所有被压制的异常。
这就是官方明确推荐**用 `try-with-resources` 代替 `finally` 关资源**的核心理由。
## 三种写法可以混用
`try-with-resources` 不排斥 `catch` 和 `finally`,三者可以同时出现:
```java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class MixDemo {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("demo.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
System.out.println("出错:" + e.getMessage());
} finally {
System.out.println("收尾");
}
}
}
执行顺序是:try 块 → 自动 close → catch(如果有异常)→ finally。
资源在 catch 和 finally 之前就已经关闭了,这一点要记牢。
资源变量是隐式 final 的
try(...) 括号里声明的变量不能被重新赋值。
// 编译报错
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
br = new BufferedReader(new FileReader("b.txt")); // 不允许
}
```java
道理很直接:JVM 记住了要关闭哪个对象。如果中途允许把变量指向别的对象,那到底该关哪个?会关错。
Java 9 起放宽了一点:**已经是 final 或事实上不再变化的外部变量,可以直接写进括号里**。
```java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Java9Resource {
static void demo(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
// Java 9+:直接引用已有变量,不用重新声明
try (br) {
System.out.println(br.readLine());
}
}
}
Java 9 之前只能写成 try (BufferedReader r = br),多绕一道。
常见误用
误用一:把不需要关闭的东西放进去。
// 没必要
try (Scanner sc = new Scanner(System.in)) {
System.out.println(sc.nextLine());
}
```bash
`Scanner` 确实实现了 `AutoCloseable`,但这里包着的是 `System.in`。关掉它之后,**整个程序后续都读不到标准输入了**。控制台输入场景下,`Scanner` 通常不该放进 `try-with-resources`。
**误用二:以为它能捕获异常。**
```java
// try-with-resources 不等于 try-catch
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
System.out.println(br.readLine());
}
// 这个方法仍然必须声明 throws IOException
它只负责关闭,不负责捕获。异常照样往外抛,该写 catch 还得写。
误用三:在 try 块里手动再关一次。
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
System.out.println(br.readLine());
br.close(); // 多余
}
```bash
不会报错(大多数 `close()` 实现是幂等的),但纯属画蛇添足。交给 JVM 就行。
## 小结
`finally` 保证清理代码一定执行,包括有 `return` 的情况——`return` 会等 `finally` 跑完才真正返回。
`finally` 里绝对不要写 `return` 或 `throw`,它们会覆盖返回值、吞掉异常。
关闭资源统一用 `try-with-resources`:资源写在 `try` 后的括号里,实现 `AutoCloseable` 即可,逆序自动关闭。
它还顺手解决了异常屏蔽问题,主异常保留,`close()` 的异常挂在 `getSuppressed()` 里,一个都不丢。