1. 程式人生 > >try-with-resource:自動地關閉資源

try-with-resource:自動地關閉資源

一、資源關閉背景

我們知道,在Java程式設計過程中,如果打開了外部資源(檔案、資料庫連線、網路連線等),我們必須在這些外部資源使用完畢後,手動關閉它們。

因為外部資源不由JVM管理,無法享用JVM的垃圾回收機制,如果我們不在程式設計時確保在正確的時機關閉外部資源就會導致外部資源洩露緊接著就會出現檔案被異常佔用,資料庫連線過多導致連線池溢位等諸多很嚴重的問題。

 二、JDK7之前的資源關閉方式

為了確保外部資源一定要被關閉,通常關閉程式碼被寫入finally程式碼塊中,當然我們還必須注意到關閉資源時可能丟擲的異常,於是變有了下面的經典程式碼:

public static void main(String[] args) {
    FileInputStream inputStream = null;
    try {
        inputStream = new FileInputStream(new File("test"));
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
    }
}

熟悉其他語言的朋友可能會開始吐槽了,在C++中,我們可以把關閉資源的程式碼放在解構函式中,在C#中,我們有using程式碼塊。這些語法都有一個共同的特性,讓外部資源的關閉行為與外部資源的控制代碼物件的生命週期關聯,當外部資源的控制代碼物件生命週期終結時(例如控制代碼物件已出作用域),外部資源的關閉行為將被自動呼叫。這樣不僅更加符合面向物件的程式設計理念(將關閉外部資源的行為內聚在外部資源的控制代碼物件中),也讓程式碼更加簡潔易懂。怎麼到了Java這裡,就找不到自動關閉外部資源的語法特性了呢。

 三、JDK7及其之後的資源關閉方式

3.1 try-with-resource語法

確實,在JDK7以前,Java沒有自動關閉外部資源的語法特性,直到JDK7中新增了try-with-resource語法,才實現了這一功能。

那什麼是try-with-resource呢?

簡而言之,當一個外部資源的控制代碼物件(比如FileInputStream物件)實現了AutoCloseable介面,

那麼就可以將上面的板式程式碼簡化為如下形式:

public static void main(String[] args) {
    try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

將外部資源的控制代碼物件的建立放在try關鍵字後面的括號中,當這個try-catch程式碼塊執行完畢後,Java會確保外部資源的close方法被呼叫。程式碼是不是瞬間簡潔許多!

 3.2 實現原理

try-with-resource並不是JVM虛擬機器的新增功能,只是JDK實現了一個語法糖,當你將上面程式碼反編譯後會發現,其實對JVM虛擬機器而言,它看到的依然是之前的寫法:

public static void main(String[] args) {
    try {
        FileInputStream inputStream = new FileInputStream(new File("test"));
        Throwable var2 = null;

        try {
            System.out.println(inputStream.read());
        } catch (Throwable var12) {
            var2 = var12;
            throw var12;
        } finally {
            if (inputStream != null) {
                if (var2 != null) {
                    try {
                        inputStream.close();
                    } catch (Throwable var11) {
                        var2.addSuppressed(var11);
                    }
                } else {
                    inputStream.close();
                }
            }

        }

    } catch (IOException var14) {
        throw new RuntimeException(var14.getMessage(), var14);
    }
}

 

 3.3 異常抑制

通過反編譯的程式碼,大家可能注意到程式碼中有一處對異常的特殊處理:

var2.addSuppressed(var11);

這是try-with-resource語法涉及的另外一個知識點,叫做異常抑制。當對外部資源進行處理(例如讀或寫)時,如果遭遇了異常,且在隨後的關閉外部資源過程中,又遭遇了異常,那麼你catch到的將會是對外部資源進行處理時遭遇的異常,關閉資源時遭遇的異常將被“抑制”但不是丟棄,通過異常的getSuppressed方法,可以提取出被抑制的異常。

 3.4 try-with-resources語句中宣告一個或多個資源

您可以在try-with-resources語句中宣告一個或多個資源。以下示例檢索zip檔案中打包的檔案的名稱,zipFileName並建立包含這些檔名稱的文字檔案:

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());
        }
    }
}

In this example, the try-with-resources statement contains two declarations that are separated by a semicolon: ZipFile and BufferedWriter. When the block of code that directly follows it terminates, either normally or because of an exception, the close methods of the BufferedWriter and ZipFile objects are automatically called in this order. Note that the close methods of resources are called in the opposite order of their creation.

在此示例中,try-with-resources語句包含以分號分隔的兩個宣告ZipFileBufferedWriter

當直接跟隨它的程式碼塊正常或由於異常而終止時,將按 BufferedWriter 和 ZipFile 物件的close方法順序自動呼叫

請注意,資源的關閉方法按其建立的相反順序呼叫。

以下示例使用try-with-resources語句自動關閉java.sql.Statement物件:

public static void viewTable(Connection con) throws SQLException {

    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";

    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);

        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");

            System.out.println(coffeeName + ", " + supplierID + ", " + 
                               price + ", " + sales + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

java.sql.Statement此示例中使用的資源是JDBC 4.1及更高版本API的一部分。

注意

try-with-resources語句可以像普通try語句一樣擁有catchfinally語塊

try-with-resources語句中,任何catchfinally塊在宣告的資源關閉後執行。

四、總結

1、當一個外部資源的控制代碼物件實現了AutoCloseable介面,JDK7中便可以利用try-with-resource語法更優雅的關閉資源,消除板式程式碼。

2、try-with-resource時,如果對外部資源的處理和對外部資源的關閉均遭遇了異常,“關閉異常”將被抑制,“處理異常”將被丟擲,但“關閉異常”並沒有丟失,而是存放在“處理異常”的被抑制的異常列表中。

參考連結:https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html