finally 与 try-with-resources
本教程共 100 篇 · 第 67 篇 · 更新于 2026-08-05 · 约 18 分钟阅读
本节目标:弄清 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("后续代码");
}
}
输出:
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 照样执行");
}
}
}
输出:
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; // 危险写法
}
}
}
答案是 2。
try 里的 return 1 被 finally 里的 return 2 直接顶掉了。更糟的是,如果 try 里抛了异常,finally 里的 return 还会把异常也一起吞掉。
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 里的异常");
}
}
}
运行只会看到 IllegalStateException。catch 里那个 RuntimeException 消失了。
原因很简单:一个方法同一时刻只能抛出一个异常。finally 后发生、后抛出,就把前面的顶掉了。被顶掉的那个叫「被屏蔽的异常」(Suppressed Exception)。
这是老式 finally 写法的先天缺陷。而 try-with-resources 恰好解决了它。
老式资源关闭有多啰嗦
Java 7 之前,关闭文件流是这个画风:
// 历史兼容: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();
}
}
}
没有 finally,没有判 null,没有手写 close()。
不管 try 块是正常结束还是抛异常退出,JVM 保证会调用资源的 close() 方法。
声明多个资源
多个资源用分号隔开,写在同一个括号里:
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;
}
JDK 里的 IO 流、Scanner、ZipFile、JDBC 的 Connection 和 Statement,全都实现了它,直接用就行。
你自己的类也可以实现:
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 关闭了
逆序关闭看得清清楚楚。
Tip
java.io.Closeable是AutoCloseable的子接口,区别只在close()声明抛出的异常类型:Closeable抛IOException,AutoCloseable抛Exception。自己写类时优先实现AutoCloseable。
被压制的异常去哪了
try-with-resources 对异常屏蔽的处理,比老式 finally 高明得多。
假设 try 块抛了异常,随后 close() 也抛了异常。老式写法里前者会被后者顶掉,而 try-with-resources 保留 try 块的异常作为主异常,把 close() 的异常「压制」起来挂在主异常上。
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());
}
}
}
}
输出:
主异常:干活时出错
被压制的异常:关闭时出错
两个异常一个都没丢。getSuppressed() 返回一个数组,装着所有被压制的异常。
这就是官方明确推荐用 try-with-resources 代替 finally 关资源的核心理由。
三种写法可以混用
try-with-resources 不排斥 catch 和 finally,三者可以同时出现:
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")); // 不允许
}
道理很直接:JVM 记住了要关闭哪个对象。如果中途允许把变量指向别的对象,那到底该关哪个?会关错。
Java 9 起放宽了一点:已经是 final 或事实上不再变化的外部变量,可以直接写进括号里。
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());
}
Scanner 确实实现了 AutoCloseable,但这里包着的是 System.in。关掉它之后,整个程序后续都读不到标准输入了。控制台输入场景下,Scanner 通常不该放进 try-with-resources。
误用二:以为它能捕获异常。
// 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(); // 多余
}
不会报错(大多数 close() 实现是幂等的),但纯属画蛇添足。交给 JVM 就行。
小结
finally 保证清理代码一定执行,包括有 return 的情况——return 会等 finally 跑完才真正返回。
finally 里绝对不要写 return 或 throw,它们会覆盖返回值、吞掉异常。
关闭资源统一用 try-with-resources:资源写在 try 后的括号里,实现 AutoCloseable 即可,逆序自动关闭。
它还顺手解决了异常屏蔽问题,主异常保留,close() 的异常挂在 getSuppressed() 里,一个都不丢。