Java语法糖 : 使用 try-with-resources 语句安全地释放资源

4,427 阅读5分钟

先给出本文的重点:

  1. 这里所谓的资源(resource)是指在程序完成后,必须关闭的对象, try-with-resources 语句确保了每个资源在语句结束时关闭;

  2. 使用 Java 7 新增的 try-with-resources 语句 代替 try-finally 语句进行资源关闭,不仅代码更精简而且更安全;

  3. 支持 try-with-resources 语句 的类必须都实现 AutoCloseable接口,同样的,我们自定义的类也可以实现这个接口来帮助我们进行一些安全的自动化释放资源;

  4. Java 9 对 try-with-resources 语句进行了改进,如果你有一个资源是 final 或等效于 final 变量, 则可以在 try-with-resources 语句中使用该变量,无需在 try-with-resources 语句中再声明一个新的变量。

下面就通过几个简单而实用的例子,给大家演示一下 try-with-resources 语句的各种用法。


Java 7 之前的 try-finally 语句

之前操作资源,为了防止因为异常造成无法关闭资源,都是通过 try-finally 语句来关闭资源流的。

这样做有两个弊端:

  1. 代码丑陋
  2. 不安全

例如下面读写文件的一个方法,需要定义大量的变量,以及反复的异常捕捉和close操作。

public static void method1() {

    FileWriter fileWriter = null;
    BufferedWriter bufferedWriter = null;
    FileReader fileReader = null;
    BufferedReader bufferedReader = null;
    File file = new File("try-with-resources-demo.txt");

    try {


        fileWriter = new FileWriter(file);
        bufferedWriter = new BufferedWriter(fileWriter);

        fileReader = new FileReader(file);
        bufferedReader = new BufferedReader(fileReader);

        bufferedWriter.write("now is:" + LocalDateTime.now() + "\n\r");
        bufferedWriter.write("availableProcessors are : " + Runtime.getRuntime().availableProcessors() + "\n\r");
        bufferedWriter.write("totalMemory is : " + Runtime.getRuntime().totalMemory() + "\n\r");
        bufferedWriter.write("maxMemory is : " + Runtime.getRuntime().maxMemory() + "\n\r");
        bufferedWriter.write("freeMemory is : " + Runtime.getRuntime().freeMemory() + "\n\r");
        bufferedWriter.flush();

        StringBuffer readResult = new StringBuffer("");
        String oneLine = null;
        while (null != (oneLine = bufferedReader.readLine())) {
            readResult.append(oneLine + "\n\r");
        }
        System.out.println(readResult.toString());


    } catch (IOException ioe) {
        //TODO log: IOException
        ioe.printStackTrace();
    } finally {
        try {
            if (null != fileReader)
                fileReader.close();
        } catch (IOException ioe) {
            //TODO log: close stream has an  IOException
            ioe.printStackTrace();
        }

        try {
            if (null != bufferedReader)
                bufferedReader.close();
        } catch (IOException ioe) {
            //TODO log: close stream has an  IOException
            ioe.printStackTrace();
        }

        try {
            if (null != fileWriter)
                fileWriter.close();
        } catch (IOException ioe) {
            //TODO log: close stream has an  IOException
            ioe.printStackTrace();
        }

        try {
            if (null != bufferedWriter)
                bufferedWriter.close();
        } catch (IOException ioe) {
            //TODO log: close stream has an  IOException
            ioe.printStackTrace();
        }

    }
}

这样的程序,显然不是有代码洁癖的小伙伴可以接受的。

try-with-resources 语句

而使用 try-with-resources 语句实现的话,就简洁了很多。

 public static void method2() {
        File file = new File("try-with-resources-demo.txt");
        try (
                FileWriter fileWriter = new FileWriter(file);
                BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
                FileReader fileReader = new FileReader(file);
                BufferedReader bufferedReader = new BufferedReader(fileReader);
        ) {
            bufferedWriter.write("now is:" + LocalDateTime.now() + "\n\r");
            bufferedWriter.write("availableProcessors are : " + Runtime.getRuntime().availableProcessors() + "\n\r");
            bufferedWriter.write("totalMemory is : " + Runtime.getRuntime().totalMemory() + "\n\r");
            bufferedWriter.write("maxMemory is : " + Runtime.getRuntime().maxMemory() + "\n\r");
            bufferedWriter.write("freeMemory is : " + Runtime.getRuntime().freeMemory() + "\n\r");
            bufferedWriter.flush();


            StringBuffer readResult = new StringBuffer("");
            String oneLine = null;
            while (null != (oneLine = bufferedReader.readLine())) {
                readResult.append(oneLine + "\n\r");
            }
            System.out.println(readResult.toString());

        } catch (IOException ioe) {
            //TODO log: IOException
            ioe.printStackTrace();
        }
    }


实现 AutoCloseable 接口

跟踪源码你会发现,使用 try-with-resources 语句自动关闭资源的类都实现了AutoCloseable 接口。

AutoCloseable 接口只有一个无参的close()方法,使用try-with-resources 语句声明的资源,只要实现了这个方法,就可以在抛出异常之前,调用close()方法进行资源关闭操作。

下面提供了一个使用线程池来执行任务的ExecutorServiceAutoCloseable,这个类实现了 AutoCloseable 接口的close()方法,可以在异常抛出以后,关闭线程池,从而达到释放线程资源的目的。

package net.ijiangtao.tech.designskill.trywithresources;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * AutoCloseable Thread pool
 *
 * @author ijiangtao
 * @create 2019-05-13 13:08
 **/
public class ExecutorServiceAutoCloseable implements AutoCloseable {

    private ExecutorService pool;
    private int poolSize;

    public ExecutorServiceAutoCloseable() {
        poolSize = Runtime.getRuntime().availableProcessors();
        pool = Executors.newFixedThreadPool(poolSize);
    }

    public void execute(Runnable runnable) {
        if (pool.isShutdown())
            throw new UnsupportedOperationException("pool isShutdown now");
        pool.execute(runnable);
    }

    @Override
    public void close() throws Exception {
        System.out.println("auto close now !!!!!!!!!!! ");
        pool.shutdown();
    }

    public static void main(String[] args) {
        try (ExecutorServiceAutoCloseable executorServiceAutoCloseable = new ExecutorServiceAutoCloseable();) {
            executorServiceAutoCloseable.execute(new Runnable() {
                @Override
                public void run() {
                    Integer.parseInt("test auto close");
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

下面是输出的结果,可以看到在抛出异常之前,先执行了close()方法来关闭资源。

auto close now !!!!!!!!!!! 
Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: For input string: "test auto close"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.parseInt(Integer.java:615)
	at net.ijiangtao.tech.designskill.trywithresources.ExecutorServiceAutoCloseable$1.run(ExecutorServiceAutoCloseable.java:39)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

这样做的目的是,当程序因为异常而结束的时候,不需要显式地关闭线程池,而可以自动关闭,从而尽快释放线程资源,降低内存消耗。

这里要注意的是,程序结束就关闭线程池,这样做的好处是占用内存小,坏处是每次执行程序都要重新创建线程池,这是有一定性能消耗的。

因此要根据具体情况而定。通常,如果程序运行频繁,则保留线程池中线程,反复使用,减少因反复创建和销毁线程造成的性能消耗。而如果程序运行结束以后,短时间内不会再次运行,则可以将线程池关闭,释放掉占用的内存。

当然也可以通过设置核心线程数和最大线程数,以及过期时间来设置自己的线程管理策略。

具体用法可以参考这篇文章:使用ThreadPoolExecutor构造线程池

Java 9 final 变量

在Java 9 中,对 try-with-resources 语句的语法进行了进一步的精简。

如果你有一个资源是 final 或等效于 final 变量, 那么你可以在 try-with-resources 语句中使用该变量,而无需在 try-with-resources 语句中声明一个新变量。

Java 7 和 Java 8 中的写法:

private static String readDataJava7(String message) throws IOException {
    Reader reader = new StringReader(message);
    BufferedReader bufferedReader = new BufferedReader(reader);
    try (BufferedReader bufferedReader2 = bufferedReader) {
        return bufferedReader2.readLine();
    }
}

Java 9 支持的写法:

    private static String readDataJava9(String message) throws IOException {
        Reader reader = new StringReader(message);
        BufferedReader bufferedReader = new BufferedReader(reader);
        try (bufferedReader) {
            return bufferedReader.readLine();
        }
    }

总结

通过 try-with-resources 语句的的好处可以总结如下:

  1. try-with-resources 语句可以带来更加简洁的代码
  2. try-with-resources 语句可以使得资源释放更加安全
  3. 自己实现 AutoCloseable 接口并使用 try-with-resources 语句可以实现安全简洁的资源释放

Wechat-westcall