资源释放与 try-with-resources | JavaSE
资源释放与 try-with-resources
一、学习目标
完成本章后,你应该能够:
- 能够解释为什么 IO 流必须正确释放资源。
- 能够理解
close()和flush()的区别。 - 能够使用传统
try-catch-finally释放 IO 资源。 - 能够指出传统
finally方案存在的代码冗余问题。 - 能够使用
try-with-resources自动释放资源。 - 能够解释什么类型的对象可以放进 try-with-resources。
- 能够理解
AutoCloseable与Closeable的关系。 - 能够解释多个资源的关闭顺序。
- 能够理解包装流为什么通常只需要管理最外层资源。
- 能够理解 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? - [ ]
Closeable与AutoCloseable是什么关系? - [ ] 多个资源按照什么顺序关闭?
- [ ] 为什么包装流通常只需要关闭最外层?
- [ ] 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
并回答:
- 为什么不再需要
finally中手工关闭? - 两个资源按照什么顺序关闭?
- 如果复制过程中发生异常,会发生什么?
- 为什么只声明最外层缓冲流也可以管理底层文件流?
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
完成一个可靠的文件复制程序。
达到以上标准,本章核心内容即掌握。