Java 手动资源关闭最佳实践

最近做了一些文件上传下载的工作,有涉及到资源关闭相关的操作,因此回顾整理了下 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();
                }
            }

        }

    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值