java se好用吗_利用 Java SE 7 更好地管理资源
2011 年 5 月發布
作者:Julien Ponge
本文介紹 Java 7 針對自動資源管理問題給出的解決辦法,即 Coin 項目中提出的新語言結構 try-with-resources 語句。
:示例源文件 (zip)
簡介
典型的 Java 應用程序可以處理多種類型的資源,如文件、流、套接字和數據庫連接。必須謹慎處理這些資源,因為對它們的操作會占用系統資源。因此,需要確保即便在出錯的情況下也能釋放這些資源。實際上,不正確的資源管理是生產應用中常見的故障根源,常見的錯誤是,代碼中其他位置出現異常后,數據庫連接和文件描述符依然處于打開狀態。由于操作系統和服務器應用程序通常有一個資源上限,因此在資源耗竭時這會導致應用服務器頻繁重啟。
針對 Java 中資源管理和異常管理的正確做法已經有了很好的文檔說明。對于任何已成功初始化的資源,都需要相應地調用它的 close() 方法。這就要求嚴格遵守 try/catch/finally 塊的用法,以確保任何從資源打開時起的執行路徑最終都能調用一個方法來關閉資源。靜態分析工具(如 FindBugs)在識別此類錯誤時很有幫助。然而通常的情況是,經驗不足的開發人員和經驗豐富的開發人員都會編寫錯誤的資源管理代碼,從而導致資源泄漏甚至更嚴重的后果。
然而,應該承認,編寫正確的資源代碼需要大量采用嵌套了 try/catch/finally 塊的樣板代碼,您在后文中將看到這一點。正確編寫這種代碼本身很快就會成為難題。與此同時,Python 和 Ruby 等其他編程語言已經提供了語言級工具(即自動資源管理)來解決這一問題。
本文介紹 Java Platform, Standard Edition (Java SE) 7 針對自動資源管理問題給出的解決辦法,即 Coin 項目中提出的新語言結構 try-with-resources 語句。我們將看到,該語句的好處遠不止像 Java SE 5 的循環語句增強一樣地加入更多語法糖。實際上,異常會彼此屏蔽,從而導致有時難以找到問題的根源。
本文首先將概述資源和異常管理,然后將從 Java 開發人員的視角介紹 try-with-resources 語句的要點。隨后將展示如何準備一個類,使之支持此類語句。接下來,將討論異常屏蔽的問題,以及 Java SE 7 做了哪些改變來解決此類問題。最后,本文將揭開語言擴展背后語法糖的神秘面紗,進行討論并給出結論。
注意:本文所述示例的源代碼可從這里下載:sources.zip
管理資源和異常
我們先從下面節選的一段代碼開始: private void incorrectWriting() throws IOException {
DataOutputStream out = new DataOutputStream(new FileOutputStream("data"));
out.writeInt(666);
out.writeUTF("Hello");
out.close();
}
乍一看,此方法似乎不會造成什么損害:它打開一個名為 data 的文件,隨后寫入一個整數和一個字符串。java.io 程序包中對流類的設計使之能夠通過修飾設計模式進行組合。
例如,我們可以在 DataOutputStream 與 FileOutputStream 之間添加一個用于壓縮數據的輸出流。關閉一個流時,也會關閉它所修飾的流。重新回到這個示例,在對 DataOutputStream 的實例調用 close() 時,同樣也會調用 FileOutputStream 的 close() 方法。
然而,關于在這種方法中對 close() 方法的調用存在一個嚴重的問題。假設在寫入整數或字符串時因底層文件系統已滿而拋出一個異常。那么,將不再有機會調用 close() 方法。
這對于 DataOutputStream 不是什么嚴重的問題,因為它僅對 OutputStream 實例進行操作,用于將基本數據類型解碼并把它們寫入字節數組中。真正的問題在于 FileOutputStream,因為它在一個文件描述符內部保留了一個操作系統資源,僅在調用 close() 時才能釋放該資源。因此,這種方法會泄漏資源。
這個問題對短時運行的程序基本無礙,但對于建立在 Java Platform, Enterprise Edition (Java EE) 應用服務器上的、長期運行的應用程序來說,由于達到了底層操作系統所允許打開的文件描述符的最大數量,可能會導致整個服務器重啟。
一種正確地重寫前述方法的方式如下: private void correctWriting() throws IOException {
DataOutputStream out = null;
try {
out = new DataOutputStream(new FileOutputStream("data"));
out.writeInt(666);
out.writeUTF("Hello");
} finally {
if (out != null) {
out.close();
}
}
}
在任何情況下,拋出的異常都會傳播給方法的調用者,但 try 塊后的 finally 塊能確保調用數據輸出流的 close() 方法。這相應地確保了底層文件輸出流的 close() 方法同樣獲得調用,從而正確釋放與文件關聯的操作系統資源。
適合缺乏耐心者的 try-with-resources 語句
不可否認,前例中存在大量確保正確關閉資源的樣板代碼。如果存在更多的流、網絡套接字或 Java 數據庫連接 (JDBC) 連接,此類樣板代碼會使您更難以閱讀一個方法的業務邏輯。更糟糕的是,它需要開發人員的自律,因為在編寫錯誤處理和資源關閉邏輯時非常容易出錯。
與此同時,其他編程語言已經引入了簡化此類情況處理的結構。例如,上一個方法可以使用 Ruby 寫成如下所示的樣子: def writing_in_ruby
File.open('rdata', 'w') do |f|
f.write(666)
f.write("Hello")
end
end
用 Python 可寫成這個樣子: def writing_in_python():
with open("pdata", "w") as f:
f.write(str(666))
f.write("Hello")
在 Ruby 中,File.open 執行了一個代碼塊,即便在該塊的執行出現異常時也能確保關閉所打開的文件。
Python 的示例與之相似,其特殊的 with 語句采用一個帶有 close 方法和一個代碼塊的對象。同樣,無論是否拋出異常,都能確保正確關閉資源。
Java SE 7 在 Coin 項目中引入了類似的語言結構。之前的示例可重寫為如下所示: private void writingWithARM() throws IOException {
try (DataOutputStream out
= new DataOutputStream(new FileOutputStream("data"))) {
out.writeInt(666);
out.writeUTF("Hello");
}
}
新結構擴展了 try 塊,按照與 for 循環相似的方式聲明了資源。在 try 塊中聲明打開的任何資源都會關閉。因此,這個新結構使您不必配對使用 try 塊與對應的 finally 塊,后者專用于正確的資源管理。使用分號分隔每個資源,例如: try (
FileOutputStream out = new FileOutputStream("output");
FileInputStream ?in1 = new FileInputStream(“input1”);
FileInputStream ?in2 = new FileInputStream(“input2”)
) {
// Do something useful with those 3 streams!
}?? // out, in1 and in2 will be closed in any case
最后需要提到的是,這樣一條 try-with-resources 語句后面可能跟 catch 和 finally 塊,就像 Java SE 7 之前的常規 try 語句一樣。
構造可自動關閉的類
您可能已經猜到了,try-with-resources 語句無法管理所有類。Java SE 7 引入了一個新接口 java.lang.AutoCloseable。它的作用就是提供一個名為 close() 的 void 方法,該方法可能拋出一個檢查到的異常 (java.lang.Exception)。任何希望在 try-with-resources 語句中使用的類都應實現該接口。強烈建議,實現的類和子接口應聲明一種比 java.lang.Exception 更精確的異常類型,當然,更好的情況是,如果調用 close() 方法不會導致失敗,就根本不用聲明異常類型。
此類 close() 方法已經進行了改進,包含在標準 Java SE 運行時環境的許多類中,這些類包括 java.io、java.nio、javax.crypto、java.security、java.util.zip、java.util.jar、javax.net 和 java.sql packages。這種方法的主要優點在于,現有代碼可繼續像以前那樣工作,而新代碼可以輕松利用 try-with-resources 語句。
我們來看看以下示例: public class AutoClose implements AutoCloseable {
@Override
public void close() {
System.out.println(">>> close()");
throw new RuntimeException("Exception in close()");
}
public void work() throws MyException {
System.out.println(">>> work()");
throw new MyException("Exception in work()");
}
public static void main(String[] args) {
try (AutoClose autoClose = new AutoClose()) {
autoClose.work();
} catch (MyException e) {
e.printStackTrace();
}
}
} class MyException extends Exception {
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
}
AutoClose 類實現了 AutoCloseable,因此可用作 try-with-resources 語句的一部分,如 main() 方法中所示。我們特意添加了一些控制臺輸出,并在該類的 work() 和 close() 方法中拋出異常。運行該程序將產生以下輸出: >>> work()
>>> close()
MyException: Exception in work()
at AutoClose.work(AutoClose.java:11)
at AutoClose.main(AutoClose.java:16)
Suppressed: java.lang.RuntimeException: Exception in close()
at AutoClose.close(AutoClose.java:6)
at AutoClose.main(AutoClose.java:17)
輸出顯然證實了在進入應處理異常的 catch 塊之前,確實調用了 close()。然而,Java 開發人員意外地發現,在 Java SE 7 中出現了以“Suppressed:(…)”為前綴的異常堆棧跟蹤行。它相當于 close() 方法拋出的異常,但在 Java SE 7 之前,您可能從未遇到過這種形式的堆棧跟蹤。這是怎么回事?
異常屏蔽
為了理解前面示例中所發生的情況,讓我們暫時拋開 try-with-resources 語句,手動重新編寫正確的資源管理代碼。首先,我們提取將由 main 方法調用的以下靜態方法: public static void runWithMasking() throws MyException {
AutoClose autoClose = new AutoClose();
try {
autoClose.work();
} finally {
autoClose.close();
}
}
隨后,相應地改造 main 方法: public static void main(String[] args) {
try {
runWithMasking();
} catch (Throwable t) {
t.printStackTrace();
}
}
現在,運行程序后會給出以下輸出: >>> work()
>>> close()
java.lang.RuntimeException: Exception in close()
at AutoClose.close(AutoClose.java:6)
at AutoClose.runWithMasking(AutoClose.java:19)
at AutoClose.main(AutoClose.java:52)
這段代碼是在 Java SE 7 之前慣用的正確資源管理方法,它顯示了一個異常被另一個異常屏蔽的問題。實際上,調用 runWithMasking() 方法的客戶端代碼將獲知 close() 方法拋出一個異常,盡管實際上是 work() 方法先拋出了異常。
然而,一次只能拋出一個異常,這就意味著在處理異常時即使正確的代碼也會遺漏一些信息。如果一個重要異常被關閉資源時進而拋出的另一個異常所屏蔽,開發人員就要浪費大量時間進行調試。敏銳的讀者可能會對此提出異議,畢竟異常是可以嵌套的。然而,僅應對彼此之間存在因果關系的異常使用嵌套,通常將一個低級異常包裝在位于應用程序架構較高層的異常中。一個很好的例子是 JDBC 驅動程序將套接字異常包裝在一個 JDBC 連接中。我們的示例中實際上有兩個異常:一個在 work() 中,一個在 close() 中,兩者之間絕對不存在因果關系。
支持“被抑制的”異常
由于異常屏蔽在實際中是如此重要的一個問題,因此 Java SE 7 擴展了異常,這樣就可以將“被抑制的”異常附加到主異常上。我們之前所說的“屏蔽的”異常實際上就是一個被抑制并附加到主異常的異常。
java.lang.Throwable 的擴展如下: public final void addSuppressed(Throwable exception) 將一個被抑制的異常附加到另一個異常上,從而避免異常屏蔽。
public final Throwable[] getSuppressed() 獲取添加到一個異常中的被抑制的異常。
這些擴展是專門為支持 try-with-resources 語句和修復異常屏蔽問題而引入的。
回到之前的 runWithMasking() 方法,我們在考慮支持被抑制異常的前提下重新編寫此方法: public static void runWithoutMasking() throws MyException {
AutoClose autoClose = new AutoClose();
MyException myException = null;
try {
autoClose.work();
} catch (MyException e) {
myException = e;
throw e;
} finally {
if (myException != null) {
try {
autoClose.close();
} catch (Throwable t) {
myException.addSuppressed(t);
}
} else {
autoClose.close();
}
}
}
很明顯,這里使用了大量代碼,其目的僅僅是正確處理一個可自動關閉類的兩個異常拋出方法!一個局部變量用于捕獲主異常,也就是 work() 方法可能拋出的異常。如果拋出這樣一個異常,則捕獲該異常,隨后立即再次拋出,以便將其余工作委托給 finally 塊。
進入 finally 塊,檢查對主異常的引用。如果拋出了一個異常,則 close() 方法可能拋出的異常將作為被抑制異常附加到此異常。否則將調用 close() 方法,如果該方法拋出一個異常,那么該異常實際上就是主異常,因此不會屏蔽其他異常。
我們來運行使用這個新方法修改后的程序: >>> work()
>>> close()
MyException: Exception in work()
at AutoClose.work(AutoClose.java:11)
at AutoClose.runWithoutMasking(AutoClose.java:27)
at AutoClose.main(AutoClose.java:58)
Suppressed: java.lang.RuntimeException: Exception in close()
at AutoClose.close(AutoClose.java:6)
at AutoClose.runWithoutMasking(AutoClose.java:34)
... 1 more
正如您所見到的那樣,我們手動重現了前文所述的 try-with-resources 語句的行為。
語法糖揭秘
我們實現的 runWithoutMasking() 方法通過正確關閉資源以及防止異常屏蔽來重現了 try-with-resources 語句的行為。實際上,Java 編譯器將以下方法的代碼擴展為與 runWithoutMasking() 代碼一致的情形,使用了 try-with-resources 語句: public static void runInARM() throws MyException {
try (AutoClose autoClose = new AutoClose()) {
autoClose.work();
}
}
可以通過反編譯來進行檢查。雖然我們可以使用 Java Development Kit (JDK) 二進制工具中包含的 javap 來比較字節碼,但我們把它當作一個字節碼到 Java 源代碼的反編譯器來使用。JD-GUI 工具提取出的 runInARM() 代碼如下(經過重新排版): public static void runInARM() throws MyException {
AutoClose localAutoClose = new AutoClose();
Object localObject1 = null;
try {
localAutoClose.work();
} catch (Throwable localThrowable2) {
localObject1 = localThrowable2;
throw localThrowable2;
} finally {
if (localAutoClose != null) {
if (localObject1 != null) {
try {
localAutoClose.close();
} catch (Throwable localThrowable3) {
localObject1.addSuppressed(localThrowable3);
}
} else {
localAutoClose.close();
}
}
}
}
可以看到,我們手動編寫的代碼使用的資源管理畫布與編譯器根據 try-with-resources 語句推斷出的畫布相同。還應注意到,編譯器處理了可能為 null 的資源引用,在 finally 塊中添加了額外的 if 語句來檢查給定資源是否為 null,從而避免對 null 引用調用 close() 時的空指針異常。我們的手動實現中并未這樣做,因為資源不可能為 null。但編譯器會系統性地生成此類代碼。
現在,我們來考慮另外一個示例,這次涉及三個資源: private static void compress(String input, String output) throws IOException {
try(
FileInputStream fin = new FileInputStream(input);
FileOutputStream fout = new FileOutputStream(output);
GZIPOutputStream out = new GZIPOutputStream(fout)
) {
byte[] buffer = new byte[4096];
int nread = 0;
while ((nread = fin.read(buffer)) != -1) {
out.write(buffer, 0, nread);
}
}
}
這個方法操縱三個資源來壓縮一個文件:一個流用于讀取、一個流用于壓縮、一個流指向輸出文件。從資源管理的視角來看,這樣的代碼是正確的。在 Java SE 7 之前,您不得不類似于下面的代碼,這段代碼是再次使用 JD-GUI 來反編譯包含此方法的類獲得的: private static void compress(String paramString1, String paramString2)
throws IOException {
FileInputStream localFileInputStream = new FileInputStream(paramString1); Object localObject1 = null;
try {
FileOutputStream localFileOutputStream = new FileOutputStream(paramString2); Object localObject2 = null;
try {
GZIPOutputStream localGZIPOutputStream = new GZIPOutputStream(localFileOutputStream); Object localObject3 = null;
try {
byte[] arrayOfByte = new byte[4096];
int i = 0;
while ((i = localFileInputStream.read(arrayOfByte)) != -1) {
localGZIPOutputStream.write(arrayOfByte, 0, i);
}
} catch (Throwable localThrowable6) {
localObject3 = localThrowable6;
throw localThrowable6;
} finally {
if (localGZIPOutputStream != null) {
if (localObject3 != null) {
try {
localGZIPOutputStream.close();
} catch (Throwable localThrowable7) {
localObject3.addSuppressed(localThrowable7);
}
} else {
localGZIPOutputStream.close();
}
}
}
} catch (Throwable localThrowable4) {
localObject2 = localThrowable4;
throw localThrowable4;
} finally {
if (localFileOutputStream != null) {
if (localObject2 != null) {
try {
localFileOutputStream.close();
} catch (Throwable localThrowable8) {
localObject2.addSuppressed(localThrowable8);
}
} else {
localFileOutputStream.close();
}
}
}
} catch (Throwable localThrowable2) {
localObject1 = localThrowable2;
throw localThrowable2;
} finally {
if (localFileInputStream != null) {
if (localObject1 != null) {
try {
localFileInputStream.close();
} catch (Throwable localThrowable9) {
localObject1.addSuppressed(localThrowable9);
}
} else {
localFileInputStream.close();
}
}
}
}
對于這樣的示例來說,Java SE 7 中的 try-with-resources 語句的好處是不言而喻的:要編寫的代碼很少、代碼的可讀性更高,最后但并非最不重要的是,代碼不會泄漏資源!
討論
java.lang.AutoCloseable 接口中 close() 方法的定義意味著可能拋出 java.lang.Exception。然而,前面的 AutoClose 示例對該方法進行聲明,但并未提及任何檢查到的異常,這是我們有意為之,部分是為了說明異常屏蔽。
可自動關閉類的規范建議避免拋出 java.lang.Exception,優先使用具體的受檢異常,如果預計 close() 方法不會失敗,就不必提及任何受檢異常。此外還建議,不要聲明任何不應被抑制的異常,java.lang.InterruptedException 就是最好的例子。實際上,抑制該異常并將其附加到另一個異常可能會導致忽略線程中斷事件,使應用程序處于不一致的狀態。
一個關于 try-with-resources 語句使用的合理問題是,與手動編寫的正確資源管理代碼相比,其對性能的影響如何。實際上并不存在性能方面的影響,因為編譯器為所有異常的正確處理推斷出盡可能少的正確代碼,正如我們在之前示例中通過反編譯所演示的那樣。
最后要說的是,try-with-resources 語句是語法糖,就像 Java SE 5 為擴展迭代器循環而引入的增強 for 循環一樣。
話雖如此,我們仍然可以限制 try-with-resources 語句擴展的復雜程度。一般來說,一個 try 塊聲明的資源越多,所生成的代碼也就越復雜。之前的 compress() 方法可重寫成僅使用兩個資源而不是三個,從而生成更精簡的異常處理塊: private static void compress(String input, String output) throws IOException {
try(
FileInputStream fin = new FileInputStream(input);
GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(output))
) {
byte[] buffer = new byte[4096];
int nread = 0;
while ((nread = fin.read(buffer)) != -1) {
out.write(buffer, 0, nread);
}
}
}
就像 Java 中出現 try-with-resources 語句之前的情況一樣,一般經驗是,開發人員在鏈接資源實例化時應始終明白需要取舍的東西。為此,最好的方法就是閱讀每個資源的 close() 方法的規范,理解其語義和影響。
回到本文最初的 writingWithARM() 示例,鏈接是安全的,因為 DataOutputStream 不可能在 close() 上拋出異常。但是,這不適用于最后一個示例,因為 GZIPOutputStream 會嘗試寫入其余壓縮數據作為 close() 方法的一部分。如果在寫入壓縮文件時,拋出異常的時間較早,GZIPOutputStream 中的 close() 方法更有可能進而拋出另一個異常,導致不會調用 FileOutputStream 中的 close() 方法,從而泄漏一個文件描述符資源。
好的做法是在 try-with-resources 語句中為每一個持有關鍵系統資源(如文件描述符、套接字或者 JDBC 連接)的每個資源進行單獨聲明,必須確保 close() 方法最終得到調用。否則,如果相關資源 API 允許,選擇鏈接分配就不僅是一種慣例:在防止資源泄漏的同時還能得到更為緊湊的代碼。
結論
本文介紹了 Java SE 7 中一種新的用于安全管理資源的語言結構。這種擴展帶來的影響不僅僅是更多的語法糖。事實上,它能位開發人員生成了正確的代碼,消除了編寫容易出錯的樣板代碼的需要。更重要的是,這種變化還伴隨著將一個異常附加到另一個異常的改進,從而為眾所周知的異常彼此屏蔽問題提供了完善的解決方案。
另請參見
下面是其他一些資源:
關于作者
Julien Ponge 是一位長期從事開源工作的技術高人。他創建了 IzPack 安裝程序框架,還參與了其他幾個項目,包括與 Sun Microsystems 合作的 GlassFish 應用服務器。他擁有 UNSW Sydney 和 UBP Clermont-Ferrand 的計算機科學博士學位,目前是 INSA de Lyon 計算機科學與工系程的副教授,并且是 INRIA Amazones 團隊的一名研究人員。由于熟練掌握行業和學術兩個領域中的語言,因此他正在積極推進這兩個領域之間更進一步的協作。
總結
以上是生活随笔為你收集整理的java se好用吗_利用 Java SE 7 更好地管理资源的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 16进制字符串转化为10进制数
- 下一篇: 微信视频号“首战”618:视频直播将有流