java編程思想讀書筆記 第十二章 通過異常處理錯誤(下)

1.異常的限制
當覆蓋方法的時候,只能拋出在基類方法的異常說明裏列出的那些異常。這意味着,當基類使用的代碼應用到其派生類對象的時候,一樣能夠工資,異常也不例外。
下面的例子是在編譯時施加在異常上面的限制:

public class BaseBallException extends Exception {}
public class Foul extends BaseBallException{}
public class Strike extends BaseBallException{}
public abstract class Inning {
    public Inning() throws BaseBallException{}
    public void event() throws BaseBallException{}
    public abstract void addBat()throws Strike,Foul;
    public void walk() {}
}
public class StormException extends Exception{}
public class RainedOut extends StormException{}
public class PopFoul extends Foul{}
public interface Storm {
    public void event() throws RainedOut;
    public void rainHard() throws RainedOut;
}
public class StromMyInnerings extends Inning implements Storm{
    /**
     * @throws BaseBallException
     */
    public StromMyInnerings() throws RainedOut,BaseBallException {}
    public StromMyInnerings(String s)throws RainedOut,BaseBallException{}   
    /**
     * @throws RainedOut
     */
    @Override
    public void rainHard() throws RainedOut {}
    /**
     * @throws Strike
     */
    @Override
    public void addBat() throws PopFoul {}
    public void event(){}
    public static void main(String[] args) {
        StromMyInnerings stromMyInnerings;
        try {
            stromMyInnerings = new StromMyInnerings();
            stromMyInnerings.addBat();
        } catch (RainedOut e) {
            System.out.println("RainedOut");
        } catch (PopFoul e) {
            System.out.println("PopFoul");
        } catch (BaseBallException e) {
            System.out.println("BaseBallException");
        }
        Inning inning;
        try {
            inning = new StromMyInnerings();
            inning.addBat();
        } catch (Strike e) {
            System.out.println("Strike");
        } catch (Foul e) {
            System.out.println("Foul");
        } catch (RainedOut e) {
            System.out.println("RainedOut");
        } catch (BaseBallException e) {
            System.out.println("BaseBallException");
        }
    }
}

在Inning類中,可以看到構造器和event()方法都聲明將拋出異常,但實際上沒有拋出。這種方式使你能強制用戶去捕獲可能在覆蓋後的event()版本中增加的異常,所以它很合理。這對於抽象方法同樣成立,如addBat()。
接口Storm值得注意,因爲它包含了一個在Inning中定義的方法event()和一個不在Inning中定義的方法rainHard()。這兩個方法都拋出新的異常RainedOut。如果StromMyInnerings類在擴展Inning類的同事又出現了Storm接口,那麼Storm裏的event()方法就不能改變Inning中的event()方法的異常接口。否則的話,在使用基類的時候就不能判斷是否捕獲了正常的異常。當然,如果接口裏定義的方法不是來自於基類,比如rainHard(),那麼此方法拋出什麼樣的異常都沒有問題。
異常限制對構造器不起作用,你會發現StromMyInnerings的構造器可以拋出任何異常,而不必理會基類構造器所拋出的異常。派生類構造器不能捕獲基類構造器拋出的異常。
儘管在繼承過程中,編譯器會對異常說明做強制要求,但異常說明本身並不屬於方法類型的一部分,方法類型是由方法的名字與參數的類型組成的。因此,不能基於異常說明來重載方法。此外,一個出現在基類方法的異常,不一定會出現在派生類方法的異常說明裏。換句話說,在繼承和覆蓋的過程中,某個特定方法的“異常說明的接口”不是變大了而是變小了–這恰好和類接口在繼承時的情形相反。
2.構造器
如果一個類繼承了某個類同時又實現了某個接口,他們有同樣的接口方法,但都拋出了不同的捕獲性異常,則該子類實現與重寫該方法時,則方法聲明處不能拋出任何捕獲性異常了。如果調用的父類構造器拋出捕獲性異常,則子類相應的構造器也只能拋出,不能在構造器裏進行捕獲。構造器拋出異常時正確的清理方式:比如在構造器中打開了一個文件,清理動作只有在對象使用完畢並且用戶調用了特殊的清理方法之後才能得以清理,而不能直接在構造器裏的finally塊上關閉,因爲finally塊是不管是否有異常都會關閉,而構造器執行成功能外界需要這個文件流。但如果在文件成功打開後才拋出異常,則需要關閉文件,並向外界拋出異常信息:

public class InputFile {
    private BufferedReader in;
    public InputFile(String fname) throws Exception{
        try {
            in = new BufferedReader(new FileReader(fname));
        } catch (FileNotFoundException e) {
            System.out.println("could not open "+fname);
            throw e;
        }catch (Exception e) {
            try {
                in.close();
            } catch (IOException e1) {
                System.out.println("in.close() unsuccessful");
                throw e;
            }finally{               
            }
        }
    }
    public String getLine(){
        String s;
        try {
            s=in.readLine();
        } catch (IOException e) {
            throw new RuntimeException("readLine() failed");
        }
        return s;
    }
    public void dispose(){
        try {
            in.close();
            System.out.println("dispose() successful");
        } catch (IOException e) {
            throw new RuntimeException("in.close() failed");
        }
}
public class CleanUp {
    public static void main(String[] args) {
        try {
            InputFile inputFile = new InputFile("D:/workspace/testjava/src/com/test/CleanUp.java");
            try {
                String s;
                int i = 1;
                while ((s = inputFile.getLine()) != null) {
                }
            } catch (Exception e) {
                System.out.println("caught Exception in main");
                e.printStackTrace(System.out);
            }finally{
                inputFile.dispose();
            }
        } catch (Exception e) {
            System.out.println("InputFile construction failed");
        }
     }
}

InputFile 的構造器接受字符串作爲參數,該字符串表示所要打開的文件名。在try塊中,會使用此文件名建立了FileReader對象。FileReader對象本身用處並不大,但可以用它來建立BufferedReader對象。注意,使用InputFile 的好處就是把兩步操作合二爲一。這種通用的清理習慣用法在構造器不拋出任何異常時也應該運用,其基本規則是:在創建需要清理的對象之後,立即進入一個try-finally語句塊。
3.異常匹配
拋出異常的時候,異常處理系統會按照代碼的書寫順序找出“最近”的處理程序。找到匹配的處理程序之後,它就認爲異常將得到處理,然後就不再繼續查找。
查找的時候並不要求拋出的異常處理程序所聲明的異常完全匹配。派生類的對象也可以匹配其基類的處理程序,例子如下:

public class Annoyance extends Exception{}
public class Sneeze extends Annoyance{}
public class Human {
    public static void main(String[] args) {
        try {
            throw new Sneeze();
        } catch (Sneeze e) {
            System.out.println("catch Sneeze");
        }catch (Annoyance e) {
            System.out.println("catch Annoyance");
        }       
        try {
            throw new Sneeze();
        }catch (Annoyance e) {
            System.out.println("catch Annoyance exception");
        }
    }
}

Sneeze異常會被第一個匹配的catch自居補貨,也就是程序裏的第一個。然而如果將這個catch字句刪掉,只留下Annoyance的catch子句,該橫向仍然能正常運行,因爲這次補貨的是Sneeze的基類。換句話說,catch(Annoyance e)會捕獲Annoyance以及所有從它派生的異常。
如果把捕獲基類的catch子句放在最前面,以此想把派生類的異常全給“屏蔽”掉,就像這樣:

try {
            throw new Sneeze();

        }catch (Annoyance e) {
            //...
        } catch (Sneeze e) {
            //.....
        }

這樣編譯器就會發現Sneeze的catch子句永遠也得不到執行,因此會報錯。

4.其他的可選方式
異常處理的一個重要的原則是“只有在你知道如何處理的情況下才捕獲異常”。實際上,異常處理的一個重要目標就是把錯誤處理的代碼同錯誤發生的地點相分離。這使你能在一段代碼中專注於要完成的事情,至於如何處理錯誤,則放在另一段代碼中。這樣以來,主幹代碼就不會與錯誤處理邏輯混在一起,也更容易理解和維護。
“被檢查的異常”可能使問題變得複雜,因爲它們強制你在可能還沒有準備好處理錯誤的時候被迫加上cacth子名,即使我們不知道如何處理的情況下,這就導致了異常的隱藏:

try{
   //… throw …
}catch(Exception e){//什麼都不做
}

把“被檢查的異常”轉換爲“不檢查的異常”:當在一個普通方法裏調用別的方法時,要考慮到“我不知道該怎樣處理這個異常,但是也不能把它‘吞’了,或者只打印一些無用的消息”。JDK1.4的異常鏈提供了一種新的思路來解決這個問題,可以直接把“被檢查的異常”包裝進RuntimeException裏面:

try{
   //… throw 檢查異常…
}catch(IDontKnowWhatToDoWithThisCheckedException e){
   Throw new RuntimeException(e);
}

如果想把“被檢查的異常”這種功能“屏蔽”掉的話,上面是一個好的辦法。不用“吞”掉異常,也不必把它放到方法的異常聲明裏面,而異常鏈還能保證你不會丟失任何原始異常的信息。你還可以在“知道如何處理的地方”來處理它,也可以其他上層catch裏通過 throw e.getCause(); 再次拋出原始的“被檢查的異常”。

5.異常使用指南
應該在以下情況使用異常:
1)在恰當的級別處理問題。(在知道該如何處理的情況下才捕獲異常。)
2)解決問題並且重新調用產生異常的方法。
3)進行少許修補,然後繞過異常產生的地方繼續執行。
4)用別的數據進行計算,以代替方法預計會返回的值。
5)把當前運行環境下能做的事情儘量做完,然後把相同的異常重拋到更高層。
6)把當前運行環境下能做的事情儘量做完,然後把不同的異常拋到更高層。
7)終止程序。
8)進行簡化。
9)讓類庫和程序更安全。

6.總結
異常時java程序設計不可分割的一部分,如果不瞭解 如何使用它們,那你只能完成很有限的工作。異常處理的有點之一就是它使得你可以在某處集中精力處理你要解決的問題,而在另一邊處理你編寫的這段代碼中產生的錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章