提示:1. 異常和錯誤處理的比較: 分離錯誤處理代碼與正常處理代碼java
2. 何時拋異常:程序員
若是方法遇到一個不知道如何處理的意外狀況(abnormal condition),那麼它應該拋出異常。編程
---》在有充足理由將某狀況視爲該方法的典型功能(typical functioning )部分時,避免使用異常。app
避免使用異常來指出能夠視爲方法的經常使用功能的狀況。
post
3. 拋什麼?spa
異常有兩種:一種非檢測異常,和檢測異常。設計
非檢測異常,如JVM拋出的, runoutofmemory, /0 相似的異常。code
檢測異常,告訴使用者,須要處理的。orm
若是發現客戶違反了契約(例如,傳入非法輸入參數),那麼拋出非檢查型異常。
若是方法沒法履型契約,那麼拋出檢查型異常,也能夠拋出非檢查型異常。
若是你認爲客戶程序員須要有意識地採起措施,那麼拋出檢查型異常。 對象
如何使用異常的原則 做者:Bill Venners著,chenkw 譯 摘要 本文是設計技術專欄文章,討論有關異常設計的問題。本文關注什麼時候使用異常,並舉例演示異常的恰當使用。此外,本文還提供一些異常設計的基本原則。 五個月前,我開始撰寫有關設計對象的文章。本文是設計文技術系列文章的延續,討論了有關錯誤報告和異常的設計原則。我假設讀者已經知道什麼是異常,以及異常是如何工做的。你若想回顧一下異常方面的知識,請閱讀本文的姐妹篇《Java異常》。 異常的好處 異常帶來諸多好處。首先,它將錯誤處理代碼從正常代碼(normal code)中分離出來。你能夠將那些執行機率爲99.9%的代碼封裝在一個try塊內,而後將異常處理代碼----這些代碼是不常常執行的----置於catch子句中。這種方式的好處是,正常代碼所以而更簡潔。 若是你不知道如何處理某個方法中的一個特定錯誤,那麼你能夠在方法中拋出異常,將處理權交給其餘人。若是你拋出一個檢查異常(checked exception),那麼Java編譯器將強制客戶程序員(cilent programmer)處理這個潛在異常,或者捕捉之,或者在方法的throws子句中聲明之。Java編譯器確保檢查異常被處理,這使得Java程序更爲健壯。 什麼時候拋出異常 異常應於什麼時候拋出?答案歸於一條原則: 若是方法遇到一個不知道如何處理的意外狀況(abnormal condition),那麼它應該拋出異常。 不幸的是,雖然這條原則易於記憶和引用,可是它並不十分清晰。實際上,它引出了另外一個的問題:什麼是意外狀況? 這是一個價值6.4萬美圓的問題。是否視某特殊事件爲「意外狀況」是一個主觀決定。其依據一般並不明顯。正由於如此,它才價值不菲。 一個更有用的經驗法則是: 在有充足理由將某狀況視爲該方法的典型功能(typical functioning )部分時,避免使用異常。 所以,意外狀況就是指方法的「正常功能」(normal functioning)以外的狀況。請容許我經過幾個例子來講明問題。 幾個例子 第一個示例使用java.io包的FileInputStream類和DataInputStream類。這是使用FileInputStream類將文件內容發送到標準輸出(standard output)的代碼: // In source packet in file except/ex9/Example9a.java import java.io.*; class Example9a { public static void main(String[] args) throws IOException { if (args.length == 0) { System.out.println("Must give filename as first arg."); return; } FileInputStream in; try { in = new FileInputStream(args[0]); } catch (FileNotFoundException e) { System.out.println("Can't find file: " + args[0]); return; } int ch; while ((ch = in.read()) != -1) { System.out.print((char) ch); } System.out.println(); in.close(); } } 在本例中,FileInputStream類的read方法報告了「已到達文件末尾」的狀況,可是,它並無採用拋出異常的方式,而是返回了一個特殊值:-1。在這個方法中,到達文件末尾被視爲方法的「正常」部分,這不是意外狀況。讀取字節流的一般方式是,繼續往下讀直到達字節流末尾。 與此不一樣的是,DataInputStream類採起了另外一種方式來報告文件末尾: // In source packet in file except/ex9b/Example9b.java import java.io.*; class Example9b { public static void main(String[] args) throws IOException { if (args.length == 0) { System.out.println("Must give filename as first arg."); return; } FileInputStream fin; try { fin = new FileInputStream(args[0]); } catch (FileNotFoundException e) { System.out.println("Can't find file: " + args[0]); return; } DataInputStream din = new DataInputStream(fin); try { int i; for (;;) { i = din.readInt(); System.out.println(i); } } catch (EOFException e) { } fin.close(); } } DataInputStream類的readInt()方法每次讀取四個字節,而後將其解釋爲一個int型數據。當讀到文件末尾時,readInt()方法將拋出EOFException。 這個方法拋出異常的緣由有二。首先,readInt()沒法返回一個特殊值來指示已經到達文件末尾,由於全部可能的返回值都是合法的整型數據。(例如,它不能採用-1這個特殊值來指示文件末尾,由於-1可能就是流中的正常數據。)其次,若是readInt()在文件末尾處只讀到一個、兩個、或者三個字節,那麼,這就能夠視爲「意外狀況」了。原本這個方法是要讀四個字節的,但只有一到三個字節可讀。因爲該異常是使用這個類時的不可分割的部分,它被設計爲檢查型異常(Exception類的子類)。客戶程序員被強制要求處理該異常。 指示「已到達末尾」狀況的第三種方式在StringTokenizer類和Stack類中獲得演示: // In source packet in file except/ex9b/Example9c.java // This program prints the white-space separated tokens of an // ASCII file in reverse order of their appearance in the file. import java.io.*; import java.util.*; class Example9c { public static void main(String[] args) throws IOException { if (args.length == 0) { System.out.println("Must give filename as first arg."); return; } FileInputStream in = null; try { in = new FileInputStream(args[0]); } catch (FileNotFoundException e) { System.out.println("Can't find file: " + args[0]); return; } // Read file into a StringBuffer StringBuffer buf = new StringBuffer(); try { int ch; while ((ch = in.read()) != -1) { buf.append((char) ch); } } finally { in.close(); } // Separate StringBuffer into tokens and // push each token into a Stack StringTokenizer tok = new StringTokenizer(buf.toString()); Stack stack = new Stack(); while (tok.hasMoreTokens()) { stack.push(tok.nextToken()); } // Print out tokens in reverse order. while (!stack.empty()) { System.out.println((String) stack.pop()); } } } 上面的程序逐字節讀取文件,將字節數據轉換爲字符數據,而後將字符數據放到StringBuffer中。它使用StringTokenizer類提取以空白字符爲分隔符的token(這裏是一個字符串),每次提取一個並壓入Stack中。最後,全部token都被從Stack中彈出並打印,每行打印一個。由於Stack類實現的是後進先出(LIFO)棧,因此,打印出來的數據順序和文件中的數據順序恰好相反。 StringTokenizer類和Stack類都必須可以指示「已到達末尾」狀況。StringTokenizer的構造方法接納源字符串。每一次調用nextToken()方法都將返回一個字符串,它是源字符串的下一個token。源字符串的全部token都必然會被消耗掉,StringTokenizer類必須經過某種方式指示已經沒有更多的token供返回了。這種狀況下,原本是能夠用一個特殊的值null來指示沒有更多token的。可是,此類的設計者採用了另外一個辦法。他提供了一個額外的方法hasMoreTokens(),該方法返回一個布爾值來指示是否已到達末尾。每次調用nextToken()方法以前,你必須先調用hasMoreTokens()。 這種方法代表設計者並不認爲到達token流的末尾是意外狀況。相反,它是使用這個類的常規狀況。然而,若是你在調用nextToken()以前不檢查hasMoreTokens(),那麼你最後會獲得一個異常NoSuchElementException。雖然該異常在到達token流末尾時拋出,但它倒是一個非檢查異常(RuntimeException的子類)。該異常的拋出不是爲了指示「已到達末尾」,而是指示一個軟件缺陷----你並無正確地使用該類。 與此相似,Stack類有一個相似的方法empty(),這個方法返回一個布爾值指示棧已經爲空。每次調用pop()以前,你都必須先調用empty()方法。若是你忘了調用empty()方法,而直接在一個空棧上調用pop()方法,那麼,你將獲得一個異常EmptyStackException。雖然該異常是棧已經爲空的狀況下拋出的,但它也是一個非檢查異常。它的做用不是檢測空棧,而是指示客戶代碼中的一個軟件缺陷(Stack類的不恰當使用)。 異常表示沒有遵照契約 經過上面的例子,你應該已經初步瞭解到,什麼時候應拋出異常而不是使用其餘方法進行通訊。若從另外一個角度來看待異常,視之爲「沒有遵照契約」,你可能對應當怎樣使用異常有更深層的理解。 面向對象程序設計中常常討論的一個設計方法是契約設計,它指出方法是客戶(方法的調用者)和聲明方法的類之間的契約。這個契約包括客戶必須知足的前置條件(precondition)和方法自己必須知足的後置條件(postcondition)。 前置條件 String類的charAt(int index)方法是一個帶有前置條件的方法。這個方法規定客戶傳入的index參數的最小取值是0,最大取值是在該String對象上調用length()方法的結果減去1。也就是說,若是字符串長度爲5,那麼index參數的取值限於0、一、二、三、4。 後置條件 String類的charAt(int index)方法的後置條件要求返回值必須是該字符串對象在index位置上的字符數據,並且該字符串對象必須保持不變。 若是客戶調用charAt()並傳入-一、和length()同樣大或者更大的值,那就認爲客戶沒有遵照契約。這種狀況下,charAt()方法是不能正確執行的,它將拋出異常StringIndexOutOfBoundsException。該異常指出客戶程序中存在某種缺陷或String類使用不當。 若是charAt()方法接收的輸入沒有問題(客戶遵照了契約),可是因爲某種緣由它沒法返回指定的索引上的字符數據(沒有知足後置條件),它將拋出異常來指示這種狀況。這種異常指出方法的實現中包含缺陷或者方法在得到運行時資源上存在問題。 所以,若是一個事件表示了「異常條件」或者「沒有遵照契約」,那麼,Java程序所要作的就是拋出異常。 拋出什麼? 一旦你決定拋出異常,你就要決定拋出什麼異常。你能夠拋出Throwable或其子類的對象。你能夠拋出Java API中定義的、或者自定義的Throwable對象。那麼,如何決定? 一般,你只須要拋出異常,而非錯誤。Error是Throwable的子類,它用於指示災難性的錯誤,好比OutOfMemoryError,這個錯誤將由JVM報告。有時一個錯誤也能夠被Java API拋出,如java.awt.AWTError。然而,在你的代碼中,你應該嚴格限制本身只拋出異常(Exception的子類)。把錯誤的拋出留給那些大牛人。 檢查型異常和非檢查型異常 如今,主要問題就是拋出檢查型異常仍是非檢查型異常了。檢查型異常是Exception的子類(或者Exception類自己),但不包括RuntimeException和它的子類。非檢查型異常是RuntimeException和它的任何子類。Error類及其子類也是檢查型的,可是你應該僅着眼於異常,你所作的應該是決定拋出RuntimeException的子類(非檢查異常)仍是Exception的子類(檢查異常)。 若是拋出了檢查型異常(而沒有捕獲它),那麼你須要在方法的throws子句中聲明該異常。客戶程序員使用這個方法,他要麼在其方法內捕獲並處理這個異常,要麼還在throws子句中拋出。檢查型異常強制客戶程序員對可能拋出的異常採起措施。 若是你拋出的是非檢查型異常,那麼客戶程序員能夠決定捕獲與否。然而,編譯器並不強制客戶程序員對非檢查型異常採起措施。事實上,他們甚至不知道可能這些異常。顯然,在非檢查型異常上客戶程序員會少費些腦筋。 有一個簡單的原則是: 若是但願客戶程序員有意識地採起措施,那麼拋出檢查型異常。 通常而言,表示類的誤用的異常應該是非檢查型異常。String類的chartAt()方法拋出的StringIndexOutOfBoundsException就是一個非檢查型異常。String類的設計者並不打算強制客戶程序員每次調用charAt(int index)時都檢查index參數的合法性。 另外一方面,java.io.FileInputStream類的read()方法拋出的是IOException,這是一個檢查異常。這個異常代表嘗試讀取文件時出錯了。這並不意味着客戶程序員錯誤地使用了FileInputStream類,而是說這個方法沒法履行它地職責,即從文件中讀出下一個字節。FileInputStream類地設計者認爲這個意外狀況很廣泛,也很重要,於是強制客戶程序員處理之。 這就是竅門所在。若是意外狀況是方法沒法履行職責,而你又認爲它很廣泛或很重要,客戶程序員必須採起措施,那麼拋出檢查型異常。不然,拋出非檢查型異常。 自定義異常類 最後,你決定實例化一個異常類,而後拋出這個異常類的實例。這裏沒有具體的規則。不要拋出用一條字符串信息指出意外狀況的Exception類,而是自定義一個異常類或者從已有異常類中選出一個合適的。那麼,客戶程序員就能夠分別爲不一樣的異常定義相應的catch語句,或者只捕獲一部分。 你可能但願在異常對象中嵌入一些信息,從而告訴catch子句該異常的更詳細信息。可是,你並不只僅依賴嵌入的信息來區別不一樣的異常。例如,你並不但願客戶程序員查詢異常對象來決定問題發生在I/O上仍是非法參數。 注意,String.charAt(int index)接收一個非法輸入時,它拋出的不是RuntimeException,甚至也不是IllegalArgumentException,而是StringIndexOutOfBoundsException。這個類型名指出問題來自字符串索引,並且這個非法索引能夠經過查詢這個異常對象而找出。 結論 本文的要點是,異常就是意外狀況,而不應用於報告那些能夠做爲方法的正常功能的狀況。雖然使用異常能夠分離常規代碼和錯誤處理代碼,從而提升代碼的可讀性,可是,異常的不恰當使用會下降代碼的可讀性。 如下是本文提出的異常設計原則: 若是方法遭遇了一個沒法處理的意外狀況,那麼拋出一個異常。 避免使用異常來指出能夠視爲方法的經常使用功能的狀況。 若是發現客戶違反了契約(例如,傳入非法輸入參數),那麼拋出非檢查型異常。 若是方法沒法履型契約,那麼拋出檢查型異常,也能夠拋出非檢查型異常。 若是你認爲客戶程序員須要有意識地採起措施,那麼拋出檢查型異常。 關於做者 Bill Venners擁有長達12年的軟件從業經驗。他以Artima軟件公司的名義在硅谷提供軟件諮詢和培訓服務。他精通不一樣平臺上的多種語言,包括針對微處理器的彙編程序設計、Unix上的C編程、Windows上的C++編程、和Web上的Java開發,所開發的軟件覆蓋了電子、教育、半導體和人身保險等行業。他是《深刻Java虛擬機》的做者。