资源释放与 try-with-resources | JavaSE

资源释放与 try-with-resources

一、学习目标

完成本章后,你应该能够:

  • 能够解释为什么 IO 流必须正确释放资源。
  • 能够理解 close()flush() 的区别。
  • 能够使用传统 try-catch-finally 释放 IO 资源。
  • 能够指出传统 finally 方案存在的代码冗余问题。
  • 能够使用 try-with-resources 自动释放资源。
  • 能够解释什么类型的对象可以放进 try-with-resources。
  • 能够理解 AutoCloseableCloseable 的关系。
  • 能够解释多个资源的关闭顺序。
  • 能够理解包装流为什么通常只需要管理最外层资源。
  • 能够理解 suppressed exception(被抑制异常)的基本思想。

二、核心知识

2.1 为什么必须释放 IO 资源

前面学习:

FileInputStream
FileOutputStream
FileReader
FileWriter
BufferedReader
BufferedWriter
Socket

这些对象并不只是普通 Java 对象。

它们背后可能持有:

文件描述符
操作系统文件句柄
Socket 连接
底层缓冲区
数据库连接

这些属于:

系统资源(Resource)

假设不断执行:

new FileInputStream(...)

却一直不关闭。

即使 Java 对象以后可以被垃圾回收:

也不能依赖 GC 及时释放底层操作系统资源

因此:

资源使用完成后应该明确关闭。


2.2 close()

大部分 IO 流都会提供:

close()

例如:

FileInputStream input =
        new FileInputStream("a.txt");

input.close();

作用:

释放该流占用的相关资源。

输出流的 close() 通常还需要完成必要的:

刷新
+
关闭

所以输出流写完数据后尤其不能随意忘记关闭。


2.3 flush() 与 close() 的区别

flush()

writer.flush();

表示:

将当前缓冲的数据继续向底层目标推进。

流仍然:

可以继续使用

例如:

writer.write("Java");

writer.flush();

writer.write("MySQL");

仍然合法。


close()

writer.close();

表示:

完成必要刷新并关闭资源。

关闭以后:

这个流原则上就不应该继续使用

因此:

flush
=
刷新,不关闭

close
=
关闭,并完成必要刷新

2.4 最简单的 close() 为什么仍然不安全

初学者容易这样写:

FileInputStream input =
        new FileInputStream("a.txt");

byte[] buffer = new byte[1024];

int len;

while ((len = input.read(buffer)) != -1) {
    System.out.println(len);
}

input.close();

看起来没问题。

但是如果:

input.read(buffer)

过程中抛出异常:

程序直接跳出正常执行流程

那么:

input.close();

可能根本执行不到。

所以真正的问题不是:

“有没有写 close()。”

而是:

异常发生时,close() 是否仍然可以可靠执行。


三、使用方法

3.1 传统 try-catch-finally

过去处理资源释放的典型方式:

FileInputStream input = null;

try {

    input = new FileInputStream("a.txt");

    byte[] buffer = new byte[1024];

    int len;

    while ((len = input.read(buffer)) != -1) {
        System.out.println(len);
    }

} catch (IOException e) {

    e.printStackTrace();

} finally {

    if (input != null) {

        try {
            input.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

核心思想:

try
→ 使用资源

catch
→ 处理异常

finally
→ 无论正常还是异常,都尝试关闭资源

3.2 finally 为什么适合资源释放

finally 的特点可以简单理解成:

正常情况下,无论 try 中代码成功还是抛出异常,finally 都会继续执行。

所以:

finally {
    input.close();
}

可以保证:

业务代码异常
      ↓
仍然有机会关闭资源

相比:

try {
    ...
}

input.close();

更加可靠。


3.3 文件复制的传统写法

假设同时存在:

InputStream input
OutputStream output

那么:

InputStream input = null;
OutputStream output = null;

try {

    input = new FileInputStream("source.jpg");
    output = new FileOutputStream("target.jpg");

    byte[] buffer = new byte[8192];

    int len;

    while ((len = input.read(buffer)) != -1) {
        output.write(buffer, 0, len);
    }

} catch (IOException e) {

    e.printStackTrace();

} finally {

    try {
        if (output != null) {
            output.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

    try {
        if (input != null) {
            input.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

看到这里应该有一个非常强烈的感觉:

“为了复制十几行数据,我为什么写了这么多资源释放代码?”

这正是:

try-with-resources

出现的重要原因。


3.4 try-with-resources

从 Java 7 开始,Java 提供:

try-with-resources

中文通常称:

带资源的 try 语句

基本语法:

try (
        资源1;
        资源2;
        ...
) {

    使用资源的代码;

}

例如:

try (
        InputStream input =
                new FileInputStream("a.txt")
) {

    byte[] buffer = new byte[1024];

    int len;

    while ((len = input.read(buffer)) != -1) {
        System.out.println(len);
    }
}

这里没有:

input.close();

但程序离开:

try

代码块时,会自动调用:

input.close();

3.5 使用 try-with-resources 复制文件

传统版本几十行代码。

现在:

import java.io.*;

public class CopyDemo {

    public static void main(String[] args) {

        try (
                InputStream input =
                        new FileInputStream("source.jpg");

                OutputStream output =
                        new FileOutputStream("target.jpg")
        ) {

            byte[] buffer = new byte[8192];

            int len;

            while ((len = input.read(buffer)) != -1) {

                output.write(buffer, 0, len);
            }

            System.out.println("复制成功");

        } catch (IOException e) {

            e.printStackTrace();
        }
    }
}

现在代码结构非常清楚:

try (
    声明资源
) {

    使用资源

} catch (...) {

    处理异常
}

资源释放:

Java 自动完成

3.6 try-with-resources 中到底能放什么

不是:

任意 Java 对象

都能放进去。

核心条件:

资源类型必须实现 AutoCloseable。

例如:

public interface AutoCloseable {

    void close() throws Exception;
}

所以只要一个对象满足:

AutoCloseable

Java 就知道:

离开 try-with-resources
        ↓
调用 close()

3.7 Closeable 与 AutoCloseable

IO 中还经常看到:

Closeable

关系:

AutoCloseable
      ↑
   Closeable

也就是:

public interface Closeable
        extends AutoCloseable

很多 IO 流实现了:

Closeable

因此也自然属于:

AutoCloseable

例如可以抽象理解:

AutoCloseable
      ↑
   Closeable
      ↑
InputStream
      ↑
FileInputStream

以及:

AutoCloseable
      ↑
   Closeable
      ↑
OutputStream
      ↑
FileOutputStream

所以它们都可以:

try ( ... ) {
}

自动关闭。


3.8 try-with-resources 可以有 catch

可以:

try (
        InputStream input =
                new FileInputStream("a.txt")
) {

    // 使用资源

} catch (IOException e) {

    e.printStackTrace();
}

也可以:

try (
        InputStream input =
                new FileInputStream("a.txt")
) {

    // 使用资源

} finally {

    // 其他收尾逻辑
}

甚至:

try (
        InputStream input =
                new FileInputStream("a.txt")
) {

} catch (IOException e) {

} finally {

}

所以:

try-with-resources 不是替代 catch。

它主要替代的是:

手工资源关闭代码

四、原理与进阶

4.1 try-with-resources 的本质

你可以把:

try (
        InputStream input =
                new FileInputStream("a.txt")
) {

    // 使用 input
}

粗略理解成编译器替你生成了:

try
    使用资源
finally
    调用 close

当然,Java 编译器实际生成的异常处理逻辑比这个更加严谨。

但学习阶段核心理解:

try-with-resources 本质上是 Java 语言帮助我们自动管理资源生命周期。


4.2 多个资源按什么顺序关闭

假设:

try (
        InputStream input =
                new FileInputStream("a.txt");

        OutputStream output =
                new FileOutputStream("b.txt")
) {

}

资源创建顺序:

1. input
2. output

关闭顺序:

1. output
2. input

也就是:

后创建的资源先关闭。

可以记成:

创建:
A → B → C

关闭:
C → B → A

这是一个:

栈式

的资源管理顺序。


4.3 为什么包装流通常只管理最外层

假设:

BufferedReader reader =
        new BufferedReader(
                new FileReader("a.txt")
        );

结构:

BufferedReader
      ↓
FileReader
      ↓
文件

如果关闭:

reader.close();

通常会继续关闭:

FileReader

所以正常写法:

try (
        BufferedReader reader =
                new BufferedReader(
                        new FileReader("a.txt")
                )
) {

}

就已经足够。

没必要故意写成:

try (
        FileReader fr =
                new FileReader("a.txt");

        BufferedReader br =
                new BufferedReader(fr)
) {

}

然后人为担心:

两个都必须手工关一次

对于典型包装流:

管理最外层流,关闭动作会沿包装链向下传播。


4.4 一个非常典型的 IO 管道

例如:

try (
        BufferedReader reader =
                new BufferedReader(
                        new InputStreamReader(
                                new FileInputStream("data.txt"),
                                StandardCharsets.UTF_8
                        )
                )
) {

}

结构:

BufferedReader
      ↓
InputStreamReader
      ↓
FileInputStream
      ↓
文件

关闭:

BufferedReader.close()

会沿着:

BufferedReader
      ↓
InputStreamReader
      ↓
FileInputStream

完成底层资源释放。

所以:

Java IO 的“包装”不仅组合功能,也组合资源生命周期。


4.5 Java 9 以后已有变量也可以作为资源

最常见写法:

try (
        BufferedReader reader =
                new BufferedReader(
                        new FileReader("a.txt")
                )
) {

}

现代 Java 还支持:

BufferedReader reader =
        new BufferedReader(
                new FileReader("a.txt")
        );

try (reader) {

    System.out.println(reader.readLine());
}

前提是:

reader

必须是:

final

或者:

effectively final

即:

创建以后没有再被重新赋值。

例如:

BufferedReader reader =
        new BufferedReader(
                new FileReader("a.txt")
        );

// reader 没有重新赋值

try (reader) {

}

这是合法的。

但如果:

reader = new BufferedReader(...);

后来又重新赋值,就不能这样使用。

对于当前学习阶段:

主写法仍建议直接在 try (...) 中声明资源。

语义最清晰。


4.6 什么是 suppressed exception

这是 try-with-resources 比传统 finally 更严谨的一个重要原因。

假设:

try 代码块中发生异常 A

同时:

close() 又发生异常 B

那么问题来了:

到底应该把哪个异常抛出去?

try-with-resources 的规则大致是:

业务异常 A
成为主要异常

close() 异常 B
成为 suppressed exception

也就是:

被抑制异常。

可以通过:

exception.getSuppressed()

获取。

这样不会因为:

资源关闭失败

而把:

最初真正导致业务失败的异常

直接覆盖掉。


4.7 为什么 try-with-resources 优于手工 finally

传统:

try {

} finally {

    resource.close();
}

存在几个问题:

代码冗长
需要 null 判断
close 本身也可能抛异常
多个资源关闭逻辑复杂
异常覆盖处理麻烦

try-with-resources:

try (
        Resource resource = ...
) {

}

优势:

代码更少
自动关闭
异常处理更加可靠
多个资源关闭顺序明确
支持 suppressed exception

所以现代 Java 开发中:

凡是适合 AutoCloseable 管理的资源,优先考虑 try-with-resources。


五、实践应用

5.1 文件复制

推荐结构:

try (
        InputStream input =
                new BufferedInputStream(
                        new FileInputStream("source.mp4")
                );

        OutputStream output =
                new BufferedOutputStream(
                        new FileOutputStream("target.mp4")
                )
) {

    byte[] buffer = new byte[8192];

    int len;

    while ((len = input.read(buffer)) != -1) {

        output.write(buffer, 0, len);
    }
}

这里已经组合:

FileInputStream
BufferedInputStream
FileOutputStream
BufferedOutputStream
try-with-resources

基本把前面 IO 的主线知识串起来了。


5.2 文本读取

try (
        BufferedReader reader =
                new BufferedReader(
                        new FileReader(
                                "article.txt",
                                StandardCharsets.UTF_8
                        )
                )
) {

    String line;

    while ((line = reader.readLine()) != null) {

        System.out.println(line);
    }
}

核心结构:

资源创建
 ↓
资源使用
 ↓
作用域结束
 ↓
自动 close

5.3 网络资源

以后学习:

Socket
ServerSocket

也会发现它们同样属于:

AutoCloseable

于是:

try (
        Socket socket = ...
) {

}

也可以使用同一套资源管理思想。

所以 try-with-resources 并不是:

“文件流专用语法。”

而是:

Java 通用资源管理机制。


5.4 JDBC 资源

以后学习 JavaWeb 和 JDBC 时会遇到:

Connection
Statement
PreparedStatement
ResultSet

这些资源也会大量使用:

try-with-resources

所以这一章其实不仅属于 IO。

它同时是在为:

JDBC
网络编程
文件操作
数据库开发

建立统一的资源生命周期管理思想。


六、常见问题

6.1 资源是什么?

在这里不要简单理解为:

任何对象

资源通常是:

使用完毕后需要明确释放外部资源或底层资源的对象。

例如:

文件流
Socket
数据库连接
Statement
ResultSet

在 Java 语法层面,如果希望放入 try-with-resources:

必须满足 AutoCloseable 要求

6.2 有 GC 为什么还要 close()?

因为:

GC
主要负责 Java 堆对象

而:

文件描述符
Socket
数据库连接
操作系统句柄

属于有限的外部资源。

不能依赖:

什么时候 GC 碰巧执行

来决定:

什么时候释放文件和网络资源

所以:

GC 不能替代 close()。


6.3 close() 与 flush() 谁更重要?

它们职责不同。

flush()
→ 刷新数据,但继续使用资源

close()
→ 完成必要刷新并关闭资源

不能把:

flush();

当作:

close();

使用。


6.4 try-with-resources 会不会吞掉异常?

不会。

例如:

try (
        InputStream input =
                new FileInputStream("not-found.txt")
) {

}

文件不存在时仍然会:

抛异常

try-with-resources 解决的是:

资源释放

不是:

取消异常

6.5 try-with-resources 还需要 catch 吗?

看需求。

可以:

public static void read() throws IOException {

    try (
            InputStream input =
                    new FileInputStream("a.txt")
    ) {

    }
}

让异常:

继续向上抛

也可以:

try (
        InputStream input =
                new FileInputStream("a.txt")
) {

} catch (IOException e) {

    e.printStackTrace();
}

所以:

try-with-resources
负责资源管理

catch / throws
负责异常处理策略

不要混成一个问题。


6.6 能不能把普通对象放进 try()?

例如:

String name = "Java";

try (name) {

}

不可以。

因为:

String

没有实现:

AutoCloseable

所以 Java 不知道:

退出 try 时应该调用什么 close()

6.7 多个资源为什么反向关闭?

假设:

A
 ↓
B
 ↓
C

后面的资源可能依赖前面的资源。

例如:

BufferedReader
依赖
InputStreamReader
依赖
FileInputStream

关闭时:

先关最外层
再关底层

更加符合依赖关系。

这就是:

创建:底层 → 外层

关闭:外层 → 底层

的思想。


6.8 为什么关闭包装流以后不要继续操作底层流?

例如:

FileOutputStream fos =
        new FileOutputStream("a.txt");

BufferedOutputStream bos =
        new BufferedOutputStream(fos);

bos.close();

此时:

bos

通常已经把:

fos

一并关闭。

所以继续:

fos.write(...)

属于错误的资源生命周期设计。

原则:

一旦把底层流交给包装流管理,就尽量通过最外层流完成整个生命周期。


七、练习与验收

7.1 知识问答

  • [ ] 为什么 IO 流必须释放资源?
  • [ ] close() 的核心作用是什么?
  • [ ] flush()close() 有什么区别?
  • [ ] 为什么简单地把 close() 写在方法最后仍可能不安全?
  • [ ] finally 为什么适合做资源释放?
  • [ ] 传统 try-catch-finally 释放资源有哪些缺点?
  • [ ] try-with-resources 从哪个 Java 版本开始提供?
  • [ ] 什么类型的对象可以放入 try-with-resources?
  • [ ] 什么是 AutoCloseable
  • [ ] CloseableAutoCloseable 是什么关系?
  • [ ] 多个资源按照什么顺序关闭?
  • [ ] 为什么包装流通常只需要关闭最外层?
  • [ ] GC 能否替代 IO 流的 close()
  • [ ] try-with-resources 是否仍然可以使用 catch
  • [ ] 什么是 suppressed exception?

7.2 代码阅读

阅读:

try (
        InputStream input =
                new FileInputStream("a.txt");

        OutputStream output =
                new FileOutputStream("b.txt")
) {

    output.write(input.readAllBytes());
}

回答:

  • [ ] 哪个资源先创建?
  • [ ] 哪个资源先关闭?
  • [ ] 是否需要显式调用 close()
  • [ ] 如果读取过程中抛异常,资源是否仍会尝试关闭?

阅读:

BufferedReader reader =
        new BufferedReader(
                new FileReader("a.txt")
        );

try (reader) {

    System.out.println(reader.readLine());
}

回答:

  • [ ] 这种写法在 JDK 21 中是否合理?
  • [ ] reader 必须满足什么变量条件?
  • [ ] 离开 try 后能否继续正常使用该 reader?

7.3 手写代码

关闭 AI 自动补全,完成:

  • [ ] 使用 try-catch-finally 完成一次文件读取和资源释放。
  • [ ] 使用 try-with-resources 重写同一程序。
  • [ ] 使用 try-with-resources 完成图片复制。
  • [ ] 同时管理输入流与输出流。
  • [ ] 使用 BufferedReader + try-with-resources 按行读取文件。
  • [ ] 使用 BufferedWriter + try-with-resources 写入文件。
  • [ ] 使用已有 effectively final 变量作为资源。

7.4 Debug

下面程序:

InputStream input =
        new FileInputStream("a.txt");

byte[] data = input.readAllBytes();

System.out.println(
        new String(data)
);

// 忘记 close

要求:

  • [ ] 找出资源管理问题。
  • [ ] 使用 try-with-resources 重写。
  • [ ] 说明为什么不能依赖 GC。

下面代码:

try (
        String text = "Java"
) {

    System.out.println(text);
}

要求:

  • [ ] 判断是否可以编译。
  • [ ] 指出根本原因。
  • [ ] 说明资源声明的类型要求。

7.5 综合训练

将下面传统文件复制程序:

InputStream input = null;
OutputStream output = null;

try {

    input = new FileInputStream("source.dat");
    output = new FileOutputStream("target.dat");

    byte[] buffer = new byte[8192];

    int len;

    while ((len = input.read(buffer)) != -1) {

        output.write(buffer, 0, len);
    }

} catch (IOException e) {

    e.printStackTrace();

} finally {

    // 手工释放 input / output
}

完整重构为:

BufferedInputStream
+
BufferedOutputStream
+
try-with-resources

并回答:

  1. 为什么不再需要 finally 中手工关闭?
  2. 两个资源按照什么顺序关闭?
  3. 如果复制过程中发生异常,会发生什么?
  4. 为什么只声明最外层缓冲流也可以管理底层文件流?

7.6 本章验收

不查看资料,能够从零写出:

try (
        Resource resource = ...
) {

    // 使用资源
}

并完整解释:

Resource
        ↓
AutoCloseable
        ↓
close()
        ↓
try-with-resources
        ↓
自动资源释放

能够区分:

GC
≠
资源关闭

能够说明:

flush
≠
close

能够解释:

多个资源:
创建 A → B → C
关闭 C → B → A

最后能够独立使用:

try-with-resources
+
BufferedInputStream
+
BufferedOutputStream

完成一个可靠的文件复制程序。

达到以上标准,本章核心内容即掌握。