新的起點 新的開始

          快樂生活 !

          異常設計

          轉之 http://blog.csdn.net/success_dream/archive/2006/11/24/1412422.aspx

          摘要

            本文是設計技術專欄文章,討論有關異常設計的問題。本文關注何時使用異常,并舉例演示異常的恰當使用。此外,本文還提供一些異常設計的基本原則。

            五個月前,我開始撰寫有關設計對象的文章。本文是設計文技術系列文章的延續,討論了有關錯誤報告和異常的設計原則。我假設讀者已經知道什么是異常,以及異常是如何工作的。你若想回顧一下異常方面的知識,請閱讀本文的姐妹篇《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、1、2、3、4。
            后置條件
            String類的charAt(int?index)方法的后置條件要求返回值必須是該字符串對象在index位置上的字符數據,而且該字符串對象必須保持不變。
             如果客戶調用charAt()并傳入-1、和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子句中聲明該異常??蛻舫绦騿T使用這個方法,他要么在其方法內捕獲并處理這個異常,要么還在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。這個類型名指出問題來自字符 串索引,而且這個非法索引可以通過查詢這個異常對象而找出。
            
          結論

            本文的要點是,異常就是意外情況,而不該用于報告那些可以作為方法的正常功能的情況。雖然使用異??梢苑蛛x常規代碼和錯誤處理代碼,從而提高代碼的可讀性,但是,異常的不恰當使用會降低代碼的可讀性。
            以下是本文提出的異常設計原則:

          如果方法遭遇了一個無法處理的意外情況,那么拋出一個異常。

          避免使用異常來指出可以視為方法的常用功能的情況。

          如果發現客戶違反了契約(例如,傳入非法輸入參數),那么拋出非檢查型異常。

          如果方法無法履型契約,那么拋出檢查型異常,也可以拋出非檢查型異常。

          如果你認為客戶程序員需要有意識地采取措施,那么拋出檢查型異常。

            
          關于作者

             Bill?Venners擁有長達12年的軟件從業經驗。他以Artima軟件公司的名義在硅谷提供軟件咨詢和培訓服務。他精通不同平臺上的多種語言, 包括針對微處理器的匯編程序設計、Unix上的C編程、Windows上的C++編程、和Web上的Java開發,所開發的軟件覆蓋了電子、教育、半導體 和人身保險等行業。他是《深入Java虛擬機》的作者。

          posted on 2007-03-16 13:39 advincenting 閱讀(388) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           

          公告

          Locations of visitors to this pageBlogJava
        1. 首頁
        2. 新隨筆
        3. 聯系
        4. 聚合
        5. 管理
        6. <2025年7月>
          293012345
          6789101112
          13141516171819
          20212223242526
          272829303112
          3456789

          統計

          常用鏈接

          留言簿(13)

          隨筆分類(71)

          隨筆檔案(179)

          文章檔案(13)

          新聞分類

          IT人的英語學習網站

          JAVA站點

          優秀個人博客鏈接

          官網學習站點

          生活工作站點

          最新隨筆

          搜索

          積分與排名

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 凤山县| 永德县| 乌拉特中旗| 文安县| 永仁县| 东丰县| 荔浦县| 静海县| 易门县| 鹤峰县| 依兰县| 仁化县| 白玉县| 万安县| 利津县| 肥城市| 西峡县| 定南县| 定远县| 兴文县| 麟游县| 兖州市| 沈丘县| 晋中市| 成安县| 洛阳市| 盘锦市| 哈巴河县| 湄潭县| 阿鲁科尔沁旗| 青川县| 漠河县| 加查县| 东宁县| 寿宁县| 临武县| 砚山县| 年辖:市辖区| 泾源县| 客服| 长治县|