Java語法糖: 使用 try-with-resources 語句安全地釋放資源

西召發表於2019-05-13

先給出本文的重點:

  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

相關文章