最近做了一些文件上传下载的工作,有涉及到资源关闭相关的操作,因此回顾整理了下 JDK 的 try-with-resrouces 资源回收方式,希望对需要的同学有所帮助,如有不足也非常欢迎交流改进。
一. 为何需要资源关闭
对于某些资源,比如 IO 流对象、Socket 套接字、数据库连接等对象,如果在使用后不手动关闭,会导致资源一直被占用,最终造成资源紧张,导致严重的性能问题。因此在使用完成后务必要将这类资源关闭。
二. 传统 try-finally 资源关闭方式
在 Java 7 之前 JDK 提供了 try-finally
的方式,通过跟在 try 块后面的 finally 代码块实现资源关闭。示例如下:
public class FileUtils {
public void readFile(File file) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(file));
try {
String content;
while ( (content = br.readLine()) != null) {
System.out.println(content);
}
} finally {
br.close();
}
}
try-finally
的资源关闭方式主要有下面几个问题:
1 .容易造成代码臃肿
上面的例子只有一个资源需要关闭,当有多个资源需要关闭时代码就会变得臃肿不堪。比如下面的例子,我需要做文件的拷贝,需要同时创建输入流和输出流,在不做异常抛出的情况下代码如下:
public class FileUtils {
public void oldCopyFile(File origin, File target) {
FileInputStream originInputStream = null;
FileOutputStream targetOutputStream = null;
try {
originInputStream = new FileInputStream(origin);
targetOutputStream = new FileOutputStream(target);
int content;
while ((content = originInputStream.read()) != -1) {
targetOutputStream.write(content);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (origin != null) {
try {
originInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (target != null) {
try {
targetOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2 .不符合控制与逻辑相分离的原则
虽然一般都会说 try-catch-finally
语句将异常处理与资源关闭从正常业务代码中分离了出来,提高了代码的质量,但就实际应用而言,资源关闭的操作始终是要和正常的业务代码在同一个方法里面的,但这部分代码其实是属于控制部分,并不属于真正的业务代码所关心的范畴。在上面的代码中,真正有效的只有处理文件读写的几行代码,其他都是为了资源关闭和异常捕获而服务的,因此更好的处理方式应该是将资源关闭的代码分离出去。
3 .影响异常堆栈轨迹
下面是 《Effective Java》中的例子:
String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
}finally {
br.close();
}
}
当底层物理设备异常时,会导致调用 readLine()
和 close()
方法时抛出异常,但此时在异常堆栈中只会存在第二个异常的记录,这会使得调试程序变得困难。
三. try-with-resources 资源关闭方式
Java 7 引入了 try-with-resources 语句来实现更简洁的资源关闭,下面是使用 try-with-resources 语句对上述拷贝文件代码的改造:
public class FileUtils {
public void newCopyFile(File origin, File target) {
try (FileInputStream originInputStream = new FileInputStream(origin);
FileOutputStream targetOutputStream = new FileOutputStream(target)){
int content;
while ((content = originInputStream.read()) != -1) {
targetOutputStream.write(content);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
通过将要关闭的资源声明在 try 后的括号中,在代码执行完成或者抛出异常时 Java 会自动将对应的资源关闭。
try-with-resources 无法关闭外部传进来的资源,可以通过新建一个变量的方式进行操作,代码如下:
public class FileUtils {
public void newCopyFile(FileInputStream input, FileOutputStream output) {
try (FileInputStream originInputStream = input;
FileOutputStream targetOutputStream = output){
int content;
while ((content = originInputStream.read()) != -1) {
targetOutputStream.write(content);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用注意
- 1 .实现 AutoCloseable 接口
在 try 中声明的需要自动关闭的资源,必须先实现 AutoCloseable 接口,该接口源代码如下,只包含一个返回类型为 void 的 close 方法。
public interface AutoCloseable {
void close() throws Exception;
}
Java 类库中的很多类和接口都实现或者扩展了 AutoCloseable 接口,比如 io 中的 Closeable 接口和 InputStream 类。
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
public abstract class InputStream implements Closeable {
...
}
- 2 . 对于需要返回的对象不要关闭
如果某个 io 流或者其他对象需要返回给外部进行使用,此时应该将资源交由调用方进行关闭。可以结合上面提到的关闭外部资源的方式,下面是一个简单的示例代码:
public InputStream getInputStream(File file) {
InputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return fileInputStream;
}
public void readFile(File file) {
InputStream inputStream = getInputStream(file);
if (Objects.isNull(inputStream)) {
return;
}
try (InputStream stream = inputStream){
stream.read();
} catch (IOException e) {
e.printStackTrace();
}
}
- 3 .某些类不需要关闭
一些类虽然实现了 AutoCloseable 接口但其方法内部是空的,比如 JDK 类库中的 ByteArrayInputStream 类,其 close 方法实现代码如下,对于这种类调用 close 方法执行资源关闭是没有意义的。因此在想要对某个资源进行关闭时,可以先看下其 close 的方法实现是否为空,为空的话就无需关心了。
public class ByteArrayInputStream extends InputStream {
public void close() throws IOException {
}
}
另外对于 Socket 中的输入输出流,也不应该被关闭,因为一旦流被关闭 Socket 连接也会被断开,如果只是关闭对应的输入输出流,应该采用 socket.shutdownOutput();
和 socket.shutdownInput();
方法。
四. try-with-resources 原理
try-with-resources
本质上是一种 Java 的语法糖,在将源代码编程为 class 文件时,Java 会将 try-with-resources
的代码翻译成传统形式的 try-finally 格式的代码然后执行,可以通过反编译 class 文件来查看最终生成的代码。看下面的例子:
- 源文件代码
public class FileUtils {
public static void copyFile(File origin, File target) throws IOException {
try (FileInputStream originInputStream = new FileInputStream(origin);
FileOutputStream targetOutputStream = new FileOutputStream(target)) {
int content;
while ((content = originInputStream.read()) != -1) {
targetOutputStream.write(content);
}
}
}
}
下面是反编译对应的 class 文件后的代码,可以看到真正执行的代码还是采用了传统的 try-finally 的方式进行了处理并且将每个异常都做了处理,避免了异常被屏蔽的情况。
public class FileUtils {
public FileUtils() {
}
public static void copyFile(File origin, File target) throws IOException {
FileInputStream originInputStream = new FileInputStream(origin);
Throwable var3 = null;
try {
FileOutputStream targetOutputStream = new FileOutputStream(target);
Throwable var5 = null;
try {
int content;
try {
while((content = originInputStream.read()) != -1) {
targetOutputStream.write(content);
}
} catch (Throwable var28) {
var5 = var28;
throw var28;
}
} finally {
if (targetOutputStream != null) {
if (var5 != null) {
try {
targetOutputStream.close();
} catch (Throwable var27) {
var5.addSuppressed(var27);
}
} else {
targetOutputStream.close();
}
}
}
} catch (Throwable var30) {
var3 = var30;
throw var30;
} finally {
if (originInputStream != null) {
if (var3 != null) {
try {
originInputStream.close();
} catch (Throwable var26) {
var3.addSuppressed(var26);
}
} else {
originInputStream.close();
}
}
}
}
}
以上就是 Java try-with-resources 的使用介绍,希望对需要的同学有所帮助。
我是 AhriJ邹同学