java try-with-resource

816 阅读5分钟

前言

Java中外部资源无法使用JVM垃圾回收机制回收,例如数据库连接、网络连接、文件等,因此使用完这些资源需要手动关闭。

Java SE 7 之前关闭外部资源

无论是try语句是正常还是异常完成,finally都会确保关闭资源,如下例子: 例一:

public static List<String> list(String filePath, Integer start, Integer limit){
        File file = new File(filePath);
        Stream<String> stream = null;
        try {
            stream = Files.lines(Paths.get(file.getPath()));
            return  stream.skip(start).limit(limit).collect(Collectors.toList());
        } catch (Exception e) {
            logger.error("exception-->fileName:{} error:{}", file.getName(), e.getMessage());
        }finally {
            if(null != stream){
                stream.close();
            }
        }
        return null;
}

从例一看到,try中stream操作异常和finally中close都抛出异常,那么方法则抛出finally异常,从try块中抛出的异常将被抑制。

Java SE 7 及之后关闭外部资源新方法

在Java SE 7 及之后, java提供了另外一种语法来关闭资源,使用try-with-resources语句,先来下例一使用此语句的写法。 例二:

public static List<String> list(String filePath, Integer start, Integer limit){
        File file = new File(filePath);
        try (Stream<String> stream  = Files.lines(Paths.get(file.getPath()))){
            return  stream.skip(start).limit(limit).collect(Collectors.toList());
        } catch (Exception e) {
            logger.error("exception-->fileName:{} error:{}", file.getName(), e.getMessage());
        }
        return null;
}

我们可以看到例二看起来比例一简洁了很多。try后面的括号里面的资源是Stream(jdk1.8标准库中提供的类),Files.lines(jdk1.8中提供的方法)方法从文件中读取所有的内容返回Stream。因为Stream是在try-with-resources语句中声明,无论try语句是正常完成还是异常停止的话,资源都会被关闭。

我们打开Stream类,可以看到类的注解上有这么一段注释:

<p>Streams have a {@link #close()} method and implement {@link AutoCloseable},
* but nearly all stream instances do not actually need to be closed after use.
* Generally, only streams whose source is an IO channel (such as those returned
* by {@link Files#lines(Path, Charset)}) will require closing.  Most streams
* are backed by collections, arrays, or generating functions, which require no
* special resource management.  (If a stream does require closing, it can be
* declared as a resource in a {@code try}-with-resources statement.)

可以看出file.lines是需要关闭流的,并且建议我们可以声明为try-with-resources来关闭文件(就像我们例二中的例子)。

另外我们看到Stream这个类继承了AutoCloseable。

那么try-with-resource语句是怎么在打开声明的资源后进行关闭的。我们进入Files.lines方法看一下:

public static Stream<String> lines(Path path, Charset cs) throws IOException {
   BufferedReader br = Files.newBufferedReader(path, cs);
   try {
       return br.lines().onClose(asUncheckedRunnable(br));
   } catch (Error|RuntimeException e) {
       try {
           br.close();
       } catch (IOException ex) {
           try {
               e.addSuppressed(ex);
           } catch (Throwable ignore) {}
       }
       throw e;
   }
}

我们看到br.lines()返回Stream, 后面调用了onClose()方法,onClose方法参数是close handler。我们看下参数代码:

/**
* Convert a Closeable to a Runnable by converting checked IOException
* to UncheckedIOException
*/
private static Runnable asUncheckedRunnable(Closeable c) {
   return () -> {
       try {
           c.close();
       } catch (IOException e) {
           throw new UncheckedIOException(e);
       }
   };
}

看到onClose方法为返回的Stream设置了close handler, 在close handler里面调用了close方法来关闭文件。

再来看下onClose方法:

/**
* Returns an equivalent stream with an additional close handler.  Close
* handlers are run when the {@link #close()} method
* is called on the stream, and are executed in the order they were
* added.  All close handlers are run, even if earlier close handlers throw
* exceptions.  If any close handler throws an exception, the first
* exception thrown will be relayed to the caller of {@code close()}, with
* any remaining exceptions added to that exception as suppressed exceptions
* (unless one of the remaining exceptions is the same exception as the
* first exception, since an exception cannot suppress itself.)  May
* return itself.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param closeHandler A task to execute when the stream is closed

* @return a stream with a handler that is run if the stream is closed
*/
S onClose(Runnable closeHandler);

我们可以看到onClose是流关闭时需要执行的任务,返回的是带有close handler的流,close handler在流关闭的时候执行。

再来看下Stream的close方法,如下所示(Stream的close方法):

/**
* Closes this stream, causing all close handlers for this stream pipeline
* to be called.
*
* @see AutoCloseable#close()
*/
@Override
void close();

注释明确表示了close方法会调用所有的close handlers。 至此我们明白了,例二就是返回了带有close handler的Stream,try-with-resource语句会自动调用资源的close方法,会触发所有的close handlers,进而实现了关闭流的操作。

try-with-resource声明多个资源,关闭顺序是怎么样的

这个我们我们可以从官方文档的例子说明下(下面的例子是从官方文档例子摘抄,可查看下面的参考文档链接): 例三:

public static void writeToFileZipFileContents(String zipFileName,
                                           String outputFileName)
                                           throws java.io.IOException {

    java.nio.charset.Charset charset =
         java.nio.charset.StandardCharsets.US_ASCII;
    java.nio.file.Path outputFilePath =
         java.nio.file.Paths.get(outputFileName);
         
    // Open zip file and create output file with 
    // try-with-resources statement
    try (
        java.util.zip.ZipFile zf =
             new java.util.zip.ZipFile(zipFileName);
        java.io.BufferedWriter writer = 
            java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
    ) {
        // Enumerate each entry
        for (java.util.Enumeration entries =
                                zf.entries(); entries.hasMoreElements();) {
            // Get the entry name and write it to the output file
            String newLine = System.getProperty("line.separator");
            String zipEntryName =
                 ((java.util.zip.ZipEntry)entries.nextElement()).getName() +
                 newLine;
            writer.write(zipEntryName, 0, zipEntryName.length());
        }
    }
}

try-with-resources被抑制的异常

另外我们再简单提一下异常抑制,在例二中如果try块和try-with-resource语句都发生异常,那么整个方法会抛出try块的异常。try-with-resource语句抛出的异常将会被抑制。在Java SE 7以及更高版本中,可以检索被抑制的异常,可以通过Throwable.getSupressed方法从try块抛出的异常检索被抑制的异常。

我们可以看下例二通过编译生成的.class文件的代码:

public static List<String> list(String filePath, Integer start, Integer limit) {
   File file = new File(filePath);
   try {
       Stream<String> stream = Files.lines(Paths.get(file.getPath()));
       Throwable var4 = null;
       List var5;
       try {
           var5 = (List)stream.skip((long)start).limit((long)limit).collect(Collectors.toList());
       } catch (Throwable var15) {
           var4 = var15;
           throw var15;
       } finally {
           if (stream != null) {
               if (var4 != null) {
                   try {
                       stream.close();
                   } catch (Throwable var14) {
                       var4.addSuppressed(var14);
                   }
               } else {
                   stream.close();
               }
           }
       }
       return var5;
   } catch (Exception var17) {
       logger.error("exception-->fileName:{} error:{}", file.getName(), var17.getMessage());
       return null;
   }
}

我们可以看到编译后的代码其实也是使用了finally块,我们来看下var4.addSuppressed(var14)这一行。从这行可以看出try-with-resource关闭资源时抛出的异常并没有丢失,而是添加到了抛出异常的Throwable集合中(List suppressedExceptions),我们可以通过抛出异常的e.getSuppressed得到被抑制的异常数组。

总结

通过上面的讲解我们对the try-with-resources statement有了一定的认识,总结为下面五点:

1.任何实现java.lang.AutoCloseable或者java.io.Closeable(Closeable继承了AutoCloseable)接口都可以作为try-with-resources资源。

2.try-with-resources语句是声明一个或者多个资源的try语句,程序完成该语句会自动调用资源的close方法来确保资源在结束时关闭。

3.try-with-resources会比Java SE 7之前通过finally块进行关闭更加简洁(通过编译后的代码可以看到也是通过fianally来实现的)。

4.try-with-resources声明的资源和try块代码如果都抛出异常,Java SE 7以及后面更高版本,可以通过Throwable.getSupressed检索被抑制的异常。

5.如果try-with-resources声明了多个资源,资源的close方法的调用和资源声明的顺序是相反的。

参考文档链接: docs.oracle.com/javase/tuto…