首页 / Java 入门教程 / finally 与 try-with-resources

Java 入门教程

finally 与 try-with-resources

本教程共 100 篇 · 第 67 篇 · 更新于 2026-08-05 · 约 18 分钟阅读

JavaJava 入门教程finallytry-with-resourcesAutoCloseable

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 一定执行后续代码

无论走不走 catchfinally 都在最后跑一遍

finally 的执行时机

finally 的执行点比想象中更「顽固」:

  • try 正常结束 → 执行 finally
  • try 抛异常且被 catch 接住 → 执行 catch,再执行 finally
  • try 抛异常但没人接 → 先执行 finally,再把异常往上抛
  • trycatch 里有 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,也不要在 finallythrow 异常。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
            }
        }
    }
}

问题一大堆:变量要提到外面声明、要判 nullclose() 自己还会抛异常。

如果同时开两个资源,还得嵌套两层 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 块 → 自动 closecatch(如果有异常)→ finally

资源在 catchfinally 之前就已经关闭了,这一点要记牢。

资源变量是隐式 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()` 里,一个都不丢。