為違例編寫代碼時,我們經常要解決的一個問題是:“一旦產生違例,會正確地進行清除嗎?”大多數時候都會非常安全,但在構建器中卻是一個大問題。構建器將對象置於一個安全的起始狀態,但它可能執行一些操作——如打開一個文件。除非用戶完成對象的使用,並調用一個特殊的清除方法,否則那些操作不會得到正確的清除。若從一個構建器內部“擲”出一個違例,這些清除行為也可能不會正確地發生。所有這些都意味著在編寫構建器時,我們必須特別加以留意。
由於前面剛學了finally,所以大家可能認為它是一種合適的方案。但事情並沒有這麼簡單,因為finally每次都會執行清除代碼——即使我們在清除方法運行之前不想執行清除代碼。因此,假如真的用finally進行清除,必須在構建器正常結束時設置某種形式的標志。而且只要設置了標志,就不要執行finally塊內的任何東西。由於這種做法並不完美(需要將一個地方的代碼同另一個地方的結合起來),所以除非特別需要,否則一般不要嘗試在finally中進行這種形式的清除。
在下面這個例子裡,我們創建了一個名為InputFile的類。它的作用是打開一個文件,然後每次讀取它的一行內容(轉換為一個字串)。它利用了由Java標准IO庫提供的FileReader以及BufferedReader類(將於第10章討論)。這兩個類都非常簡單,大家現在可以毫無困難地掌握它們的基本用法:
//: Cleanup.java // Paying attention to exceptions // in constructors import java.io.*; class InputFile { private BufferedReader in; InputFile(String fname) throws Exception { try { in = new BufferedReader( new FileReader(fname)); // Other code that might throw exceptions } catch(FileNotFoundException e) { System.out.println( "Could not open " + fname); // Wasn't open, so don't close it throw e; } catch(Exception e) { // All other exceptions must close it try { in.close(); } catch(IOException e2) { System.out.println( "in.close() unsuccessful"); } throw e; } finally { // Don't close it here!!! } } String getLine() { String s; try { s = in.readLine(); } catch(IOException e) { System.out.println( "readLine() unsuccessful"); s = "failed"; } return s; } void cleanup() { try { in.close(); } catch(IOException e2) { System.out.println( "in.close() unsuccessful"); } } } public class Cleanup { public static void main(String[] args) { try { InputFile in = new InputFile("Cleanup.java"); String s; int i = 1; while((s = in.getLine()) != null) System.out.println(""+ i++ + ": " + s); in.cleanup(); } catch(Exception e) { System.out.println( "Caught in main, e.printStackTrace()"); e.printStackTrace(); } } } ///:~
該例使用了Java 1.1 IO類。
用於InputFile的構建器采用了一個String(字串)參數,它代表我們想打開的那個文件的名字。在一個try塊內部,它用該文件名創建了一個FileReader。對FileReader來說,除非轉移並用它創建一個能夠實際與之“交談”的BufferedReader,否則便沒什麼用處。注意InputFile的一個好處就是它同時合並了這兩種行動。
若FileReader構建器不成功,就會產生一個FileNotFoundException(文件未找到違例)。必須單獨捕獲這個違例——這屬於我們不想關閉文件的一種特殊情況,因為文件尚未成功打開。其他任何捕獲從句(catch)都必須關閉文件,因為文件已在進入那些捕獲從句時打開(當然,如果多個方法都能產生一個FileNotFoundException違例,就需要稍微用一些技巧。此時,我們可將不同的情況分隔到數個try塊內)。close()方法會擲出一個嘗試過的違例。即使它在另一個catch從句的代碼塊內,該違例也會得以捕獲——對Java編譯器來說,那個catch從句不過是另一對花括號而已。執行完本地操作後,違例會被重新“擲”出。這樣做是必要的,因為這個構建器的執行已經失敗,我們不希望調用方法來假設對象已正確創建以及有效。
在這個例子中,沒有采用前述的標志技術,finally從句顯然不是關閉文件的正確地方,因為這可能在每次構建器結束的時候關閉它。由於我們希望文件在InputFile對象處於活動狀態時一直保持打開狀態,所以這樣做並不恰當。
getLine()方法會返回一個字串,其中包含了文件中下一行的內容。它調用了readLine(),後者可能產生一個違例,但那個違例會被捕獲,使getLine()不會再產生任何違例。對違例來說,一項特別的設計問題是決定在這一級完全控制一個違例,還是進行部分控制,並傳遞相同(或不同)的違例,或者只是簡單地傳遞它。在適當的時候,簡單地傳遞可極大簡化我們的編碼工作。getLine()方法會變成:
String getLine() throws IOException {
return in.readLine();
}
但是當然,調用者現在需要對可能產生的任何IOException進行控制。
用戶使用完畢InputFile對象後,必須調用cleanup()方法,以便釋放由BufferedReader以及/或者FileReader占用的系統資源(如文件句柄)——注釋⑥。除非InputFile對象使用完畢,而且到了需要棄之不用的時候,否則不應進行清除。大家可能想把這樣的機制置入一個finalize()方法內,但正如第4章指出的那樣,並非總能保證finalize()獲得正確的調用(即便確定它會調用,也不知道何時開始)。這屬於Java的一項缺陷——除內存清除之外的所有清除都不會自動進行,所以必須知會客戶程序員,告訴他們有責任用finalize()保證清除工作的正確進行。
⑥:在C++裡,“破壞器”可幫我們控制這一局面。
在Cleanup.java中,我們創建了一個InputFile,用它打開用於創建程序的相同的源文件。同時一次讀取該文件的一行內容,而且添加相應的行號。所有違例都會在main()中被捕獲——盡管我們可選擇更大的可靠性。
這個示例也向大家展示了為何在本書的這個地方引入違例的概念。違例與Java的編程具有很高的集成度,這主要是由於編譯器會強制它們。只有知道了如何操作那些違例,才可更進一步地掌握編譯器的知識。