1. 引言
在 Java 中(以及其他很多场景),与文件打交道就意味着在使用外部资源。当你打开一个文件时,操作系统会为你的程序分配一个“描述符”——一个特殊的标识符,使你可以读取和写入文件。这类描述符的数量是有限的:如果不关闭文件,程序可能会很快“吃掉”全部可用资源,并开始抛出类似 "Too many open files" 的神秘错误。
此外,如果文件未关闭,它可能会一直被其他程序锁定。比如,你以写方式打开了文件,忘了关闭,此时你和其他人都无法修改或删除它。这就像文件系统世界里的“永久劫持人质”。
生活中的例子
FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... 做了一些操作,然后忘了调用 fis.close()
如果不调用 close() 方法,文件会一直“挂着”,直到程序结束。在大型应用中,这会导致资源泄漏,甚至引发程序崩溃。
2. 旧方法:finally + close()
在 Java 7 之前,保证关闭文件的经典写法如下:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// 读取文件
int b = fis.read();
// ...
} catch (IOException e) {
System.out.println("读取文件时出错:" + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.out.println("关闭文件时出错:" + e.getMessage());
}
}
}
这种做法的缺点
- 很容易忘记写 finally 块,从而导致资源泄漏。
- 样板代码很多,尤其是有多个流时。
- 关闭时若抛出异常,也需要额外捕获处理。
- 代码臃肿、可读性下降。
3. 现代方法:try-with-resources
幸运的是,Java 7 引入了一个优雅且自动化的语法来解决这些问题 —— try-with-resources。
它长什么样
try (FileInputStream fis = new FileInputStream("data.txt")) {
int b = fis.read();
// 操作文件
} catch (IOException e) {
System.out.println("处理文件时出错:" + e.getMessage());
}
// 这里的 fis 已经被自动关闭!
关键点:所有在 try 后圆括号中声明的资源,会在代码块结束后自动关闭 —— 即使中途发生了异常。无需编写 finally,也不必单独捕获关闭时的错误 —— Java 会替你完成。
哪些类可以用在 try-with-resources 中?
任何实现了 AutoCloseable 接口(或较早的 Closeable)的类。这几乎涵盖了所有标准的 I/O 流:FileInputStream、FileOutputStream、BufferedReader、BufferedWriter、Scanner、PrintWriter 等等。
4. try-with-resources 的语法:细节与示例
单个资源
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("错误:" + e.getMessage());
}
// reader 已被自动关闭!
多个资源
可以用分号一次声明多个资源:
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
System.out.println("复制时出错:" + e.getMessage());
}
// 两个流都已关闭!
关闭顺序:资源会按照与声明相反的顺序关闭。会先调用 writer.close(),再调用 reader.close()。当一个流依赖另一个流时,这一点很重要。
与自定义类一起使用
如果你在编写自己的资源类,只需实现 AutoCloseable 接口:
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("正在使用资源!");
}
@Override
public void close() {
System.out.println("资源已关闭!");
}
}
try (MyResource res = new MyResource()) {
res.doSomething();
}
// 退出代码块后将输出: "资源已关闭!"
5. 它如何工作:示意图
flowchart TD
A[在 try-with-resources 中打开资源] --> B{try 块中是否发生异常?}
B -- 否 --> C[资源自动关闭]
B -- 是 --> D[资源自动关闭]
D --> E[异常继续向外抛出]
C --> F[程序继续执行]
结论:无论是否发生错误,资源都会被关闭!
6. 示例:把代码“改写成新风格”
之前(旧风格):
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("input.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("错误:" + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("关闭时出错:" + e.getMessage());
}
}
}
之后(try-with-resources):
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("错误:" + e.getMessage());
}
// 搞定,不需要 finally!
7. 关闭期间发生错误会怎样?
有时关闭资源本身也可能抛出异常(例如文件突然消失)。在 try-with-resources 中,这些异常不会丢失:如果在 try 块中已经有一个异常,而在关闭资源时又出现了第二个异常,它会作为被“抑制”的异常(suppressed exception)附加到主异常上。可以通过 Throwable.getSuppressed() 来查看。
示例
try (MyResource res = new MyResource()) {
throw new IOException("try 块中的错误");
} catch (IOException e) {
System.out.println("主错误:" + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("被抑制的异常:" + suppressed.getMessage());
}
}
8. 哪些类支持 try-with-resources?
很简单:任何实现了 AutoCloseable 接口的类。以下只是部分标准类:
| 类 | 用途 |
|---|---|
|
从文件读取字节 |
|
向文件写入字节 |
|
读取/写入文本 |
|
对流进行缓冲 |
|
写入带格式的文本 |
|
从文件/控制台读取数据 |
|
序列化/反序列化 |
|
处理 ZIP 压缩包 |
|
网络连接 |
如果你在使用第三方库——请查看其文档:如果有 close() 方法,那么该类很可能支持 try-with-resources。
9. 建议与有用的细节
可以在 try 之外声明变量(自 Java 9 起):如果资源是 final 或“有效 final(effectively final)”,可以复用已声明的资源:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
// ...
}
不止适用于文件:try-with-resources 适用于任何资源:网络连接、数据库、任何带有 close() 方法的对象。
不要忽视异常:即便使用了 try-with-resources,也别忘了捕获并处理异常 —— 它不是万能药,只是避免泄漏的便捷方式。
不要在 try 块内手动关闭资源:没有必要 —— Java 会替你做!如果手动调用了 close(),随后 try 块结束时还会再次尝试关闭已关闭的资源。通常这是安全的,但可能让人困惑。
10. 使用 try-with-resources 时的常见错误
错误 №1:干脆没用 try-with-resources。如果你还在写 finally { resource.close(); } —— 要么你停留在 2011 年,要么你还没读这篇讲座!请使用现代语法。
错误 №2:在 try 外声明资源,只在内部使用。这段代码不会自动关闭资源:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
// ... 使用 reader
} finally {
// 这里却忘了关闭!
}
错误 №3:在 try 块内手动调用 close()。这不致命,但多此一举,还可能导致重复关闭。把它交给 Java 吧。
错误 №4:只捕获 Exception,忽略 IO 细节。最好捕获更具体的异常(FileNotFoundException、IOException),以便给用户更清晰的提示。
错误 №5:不处理被抑制的异常。如果在关闭资源时发生了错误,它可能被“抑制”。当你分析错误时,别忘了 getSuppressed()。
GO TO FULL VERSION