Java異常架構

Java異常簡介

  Java異常是Java提供的一種識別及響應錯誤的一致性機制。
  Java異常機制可使程序中異常處理代碼和正常業務代碼分離,保證程序代碼更加優雅,並提升程序健壯性。在有效使用異常的狀況下,異常能清晰的回答what, where, why這3個問題:異常類型回答了「什麼」被拋出,異常堆棧跟蹤回答了「在哪「拋出,異常信息回答了「爲何「會拋出。java

Java異常機制用到的幾個關鍵字:try、catch、finally、throw、throws。
try        -- 用於監聽。將要被監聽的代碼(可能拋出異常的代碼)放在try語句塊以內,當try語句塊內發生異常時,異常就被拋出。
catch   -- 用於捕獲異常。catch用來捕獲try語句塊中發生的異常。
finally  -- finally語句塊老是會被執行。它主要用於回收在try塊裏打開的物力資源(如數據庫鏈接、網絡鏈接和磁盤文件)。只有finally塊,執行完成之 後,纔會回來執行try或者catch塊中的return或者throw語句,若是finally中使用了return或者throw等終止方法的語句, 則就不會跳回執行,直接中止。
throw   -- 用於拋出異常。
throws -- 用在方法簽名中,用於聲明該方法可能拋出的異常。程序員

下面經過幾個示例對這幾個關鍵字進行簡單瞭解。數據庫

示例一: 瞭解try和catch基本用法數組

public class Demo1 {

    public static void main(String[] args) {
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
              System.out.println("Caught Exception"); 
            System.out.println("e.getMessage(): " + e.getMessage()); 
            System.out.println("e.toString(): " + e.toString()); 
            System.out.println("e.printStackTrace():");
            e.printStackTrace(); 
        }
    }
}

運行結果安全

Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
    at Demo1.main(Demo1.java:6)

結果說明在try語句塊中有除數爲0的操做,該操做會拋出java.lang.ArithmeticException異常。經過catch,對該異常進行捕獲。
觀察結果咱們發現,並無執行System.out.println("i="+i)。這說明try語句塊發生異常以後,try語句塊中的剩餘內容就不會再被執行了。 網絡

示例二: 瞭解finally的基本用法架構

在"示例一"的基礎上,咱們添加finally語句。併發

public class Demo2 {

    public static void main(String[] args) {
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
              System.out.println("Caught Exception"); 
            System.out.println("e.getMessage(): " + e.getMessage()); 
            System.out.println("e.toString(): " + e.toString()); 
            System.out.println("e.printStackTrace():");
            e.printStackTrace(); 
        } finally {
            System.out.println("run finally");
        }
    }
}

運行結果app

Caught Exception
e.getMessage(): / by zero
e.toString(): java.lang.ArithmeticException: / by zero
e.printStackTrace():
java.lang.ArithmeticException: / by zero
    at Demo2.main(Demo2.java:6)
run finally

結果說明最終執行了finally語句塊。 框架

示例三: 瞭解throws和throw的基本用法

throws是用於聲明拋出的異常,而throw是用於拋出異常。

class MyException extends Exception {
    public MyException() {}
    public MyException(String msg) {
        super(msg);
    }
}

public class Demo3 {

    public static void main(String[] args) {
        try {
            test();
        } catch (MyException e) {
            System.out.println("Catch My Exception");
            e.printStackTrace();
        }
    }
    public static void test() throws MyException{
        try {
            int i = 10/0;
              System.out.println("i="+i); 
        } catch (ArithmeticException e) {
            throw new MyException("This is MyException"); 
        }
    }
}

運行結果

Catch My Exception
MyException: This is MyException
    at Demo3.test(Demo3.java:24)
    at Demo3.main(Demo3.java:13)

結果說明
   MyException是繼承於Exception的子類。test()的try語句塊中產生ArithmeticException異常(除數爲 0),並在catch中捕獲該異常;接着拋出MyException異常。main()方法對test()中拋出的MyException進行捕獲處理。

Java異常框架

Java異常架構圖


1. Throwable
  Throwable是 Java 語言中全部錯誤或異常的超類。
  Throwable包含兩個子類: ErrorException。它們一般用於指示發生了異常狀況。
  Throwable包含了其線程建立時線程執行堆棧的快照,它提供了printStackTrace()等接口用於獲取堆棧跟蹤數據等信息。

2. Exception
  Exception及其子類是 Throwable 的一種形式,它指出了合理的應用程序想要捕獲的條件。

3. RuntimeException
  RuntimeException是那些可能在 Java 虛擬機正常運行期間拋出的異常的超類。
  編譯器不會檢查RuntimeException異常。例 如,除數爲零時,拋出ArithmeticException異常。RuntimeException是ArithmeticException的超類。 當代碼發生除數爲零的狀況時,假若既"沒有經過throws聲明拋出ArithmeticException異常",也"沒有經過 try...catch...處理該異常",也能經過編譯。這就是咱們所說的"編譯器不會檢查RuntimeException異常"!
  若是代碼會產生RuntimeException異常,則須要經過修改代碼進行避免。例如,若會發生除數爲零的狀況,則須要經過代碼避免該狀況的發生!

4. Error
  和Exception同樣,Error也是Throwable的子類。它用於指示合理的應用程序不該該試圖捕獲的嚴重問題,大多數這樣的錯誤都是異常條件。
  和RuntimeException同樣,編譯器也不會檢查Error。

Java將可拋出(Throwable)的結構分爲三種類型:被檢查的異常(Checked Exception),運行時異常(RuntimeException)和錯誤(Error)。

(01) 運行時異常
定義: RuntimeException及其子類都被稱爲運行時異常。
特色: Java編譯器不會檢查它。也 就是說,當程序中可能出現這類異常時,假若既"沒有經過throws聲明拋出它",也"沒有用try-catch語句捕獲它",仍是會編譯經過。例如,除 數爲零時產生的ArithmeticException異常,數組越界時產生的IndexOutOfBoundsException異常,fail- fail機制產生的ConcurrentModificationException異常等,都屬於運行時異常。
  雖然Java編譯器不會檢查運行時異常,可是咱們也能夠經過throws進行聲明拋出,也能夠經過try-catch對它進行捕獲處理。
  若是產生運行時異常,則須要經過修改代碼來進行避免。例如,若會發生除數爲零的狀況,則須要經過代碼避免該狀況的發生!

(02) 被檢查的異常
定義Exception類自己,以及Exception的子類中除了"運行時異常"以外的其它子類都屬於被檢查異常。
特色: Java編譯器會檢查它。此 類異常,要麼經過throws進行聲明拋出,要麼經過try-catch進行捕獲處理,不然不能經過編譯。例 如,CloneNotSupportedException就屬於被檢查異常。當經過clone()接口去克隆一個對象,而該對象對應的類沒有實現 Cloneable接口,就會拋出CloneNotSupportedException異常。
  被檢查異常一般都是能夠恢復的。

(03) 錯誤
定義: Error類及其子類。
特色: 和運行時異常同樣,編譯器也不會對錯誤進行檢查。
  當資源不足、約束失敗、或是其它程序沒法繼續運行的條件發生時,就產生錯誤。程序自己沒法修復這些錯誤的。例如,VirtualMachineError就屬於錯誤。
  按照Java慣例,咱們是不該該是實現任何新的Error子類的!

對於上面的3種結構,咱們在拋出異常或錯誤時,到底該哪種?《Effective Java》中給出的建議是:對於能夠恢復的條件使用被檢查異常,對於程序錯誤使用運行時異常。

Effective Java 》中關於異常處理的幾條建議:

第1條: 只針對不正常的狀況才使用異常

建議:異常只應該被用於不正常的條件,它們永遠不該該被用於正常的控制流。
經過比較下面的兩份代碼進行說明。
代碼1

try {
    int i=0;
    while (true) {
        arr[i]=0;
        i++;
    }
} catch (IndexOutOfBoundsException e) { }

代碼2

for (int i=0; i<arr.length; i++) {
    arr[i]=0;
}

  兩份代碼的做用都是遍歷arr數組,並設置數組中每個元素的值爲0。代碼1的是經過異常來終止,看起來很是難懂,代碼2是經過數組邊界來終止。咱們應該避免使用代碼1這種方式,主要緣由有三點:
• 異常機制的設計初衷是用於不正常的狀況,因此不多會會JVM實現試圖對它們的性能進行優化。因此,建立、拋出和捕獲異常的開銷是很昂貴的。
• 把代碼放在try-catch中返回阻止了JVM實現原本可能要執行的某些特定的優化。
• 對數組進行遍歷的標準模式並不會致使冗餘的檢查,有些現代的JVM實現會將它們優化掉。

實際上,基於異常的模式比標準模式要慢得多。測試代碼以下:

public class Advice1 {

    private static int[] arr = new int[]{1,2,3,4,5};
    private static int SIZE = 10000;

    public static void main(String[] args) {

        long s1 = System.currentTimeMillis();
        for (int i=0; i<SIZE; i++)
            endByRange(arr);
        long e1 = System.currentTimeMillis();
        System.out.println("endByRange time:"+(e1-s1)+"ms" );

        long s2 = System.currentTimeMillis();
        for (int i=0; i<SIZE; i++)
            endByException(arr);
        long e2 = System.currentTimeMillis();
        System.out.println("endByException time:"+(e2-s2)+"ms" );
    }

    // 遍歷arr數組: 經過異常的方式
    private static void endByException(int[] arr) {
        try {
            int i=0;
            while (true) {
                arr[i]=0;
                i++;
                //System.out.println("endByRange: arr["+i+"]="+arr[i]);
            }
        } catch (IndexOutOfBoundsException e) {
        }
    }

    // 遍歷arr數組: 經過邊界的方式
    private static void endByRange(int[] arr) {
        for (int i=0; i<arr.length; i++) {
            arr[i]=0;
            //System.out.println("endByException: arr["+i+"]="+arr[i]);
        }
    }
}

運行結果

endByRange time:8ms
endByException time:16ms

結果說明:經過異常遍歷的速度比普通方式遍歷數組慢不少!

第2條: 對於可恢復的條件使用被檢查的異常,對於程序錯誤使用運行時異常

運行時異常     -- RuntimeException類及其子類都被稱爲運行時異常。
被檢查的異常 -- Exception類自己,以及Exception的子類中除了"運行時異常"以外的其它子類都屬於被檢查異常。

  它們的區別是:Java編譯器會對"被檢查的異常"進行檢查,而對"運行時異常"不會檢查。也 就是說,對於被檢查的異常,要麼經過throws進行聲明拋出,要麼經過try-catch進行捕獲處理,不然不能經過編譯。而對於運行時異常,假若既" 沒有經過throws聲明拋出它",也"沒有用try-catch語句捕獲它",仍是會編譯經過。固然,雖然說Java編譯器不會檢查運行時異常,可是,我 們一樣能夠經過throws對該異常進行說明,或經過try-catch進行捕獲。
  ArithmeticException(例如,除數爲0),IndexOutOfBoundsException(例如,數組越界)等都屬於運行時 異常。對於這種異常,咱們應該經過修改代碼進行避免它的產生。而對於被檢查的異常,則能夠經過處理讓程序恢復運行。例如,假設由於一個用戶沒有存儲足夠數 量的前,因此他在企圖在一個收費電話上進行呼叫就會失敗;因而就將一個被檢查異常拋出。

第3條: 避免沒必要要的使用被檢查的異常

  "被檢查的異常"是Java語言的一個很好的特性。與返回代碼不一樣,"被檢查的異常"會強迫程序員處理例外的條件,大大提升了程序的可靠性。
  可是,過度使用被檢查異常會使API用起來很是不方便。若是一個方法拋出一個或多個被檢查的異常,那麼調用該方法的代碼則必須在一個或多個catch 語句塊中處理這些異常,或者必須經過throws聲明拋出這些異常。 不管是經過catch處理,仍是經過throws聲明拋出,都給程序員添加了不可忽略的負擔。

  適用於"被檢查的異常"必須同時知足兩個條件:第一,即便正確使用API並不能阻止異常條件的發生。第二,一旦產生了異常,使用API的程序員能夠採起有用的動做對程序進行處理。

第4條: 儘可能使用標準的異常

代碼重用是值得提倡的,這是一條通用規則,異常也不例外。重用現有的異常有幾個好處:
第一,它使得你的API更加易於學習和使用,由於它與程序員原來已經熟悉的習慣用法是一致的。
第二,對於用到這些API的程序而言,它們的可讀性更好,由於它們不會充斥着程序員不熟悉的異常。
第三,異常類越少,意味着內存佔用越小,而且轉載這些類的時間開銷也越小。

Java標準異常中有幾個是常常被使用的異常。以下表格:
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ 異常                                                                  ┃ 使用場合                                                              ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ IllegalArgumentException                             │ 參數的值不合適                                                    ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IllegalStateException                                     │ 參數的狀態不合適                                                 ┃
┠───────────────────────────┼─────────────────────────────┨
┃ NullPointerException                                      │ 在null被禁止的狀況下參數值爲null                     ┃
┠───────────────────────────┼─────────────────────────────┨
┃ IndexOutOfBoundsException                       │ 下標越界                                                               ┃
┠───────────────────────────┼─────────────────────────────┨
┃ ConcurrentModificationException                │ 在禁止併發修改的狀況下,對象檢測到併發修改 ┃
┠───────────────────────────┼──────────── ─────────────────┨
┃ UnsupportedOperationException                │ 對象不支持客戶請求的方法                                   ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
  雖然它們是Java平臺庫迄今爲止最常被重用的異常,可是,在許可的條件下,其它的異常也能夠被重用。例如,若是你要實現諸如複數或者矩陣之類的算術對象,那麼重用ArithmeticException和NumberFormatException將是很是合適的。若是一個異常知足你的須要,則不要猶豫,使用就能夠,不過你必定要確保拋出異常的條件與該異常的文檔中描述的條件一致。這種重用必須創建在語義的基礎上,而不是名字的基礎上!
  最後,必定要清楚,選擇重用哪種異常並無必須遵循的規則。例 如,考慮紙牌對象的情形,假設有一個用於發牌操做的方法,它的參數(handSize)是發一手牌的紙牌張數。假設調用者在這個參數中傳遞的值大於整副牌 的剩餘張數。那麼這種情形既能夠被解釋爲IllegalArgumentException(handSize的值太大),也能夠被解釋爲 IllegalStateException(相對客戶的請求而言,紙牌對象的紙牌太少)。

第5條: 拋出的異常要適合於相應的抽象

  若是一個方法拋出的異常與它執行的任務沒有明顯的關聯關係,這種情形會讓人不知所措。當一個方法傳遞一個由低層抽象拋出的異常時,每每會發生這種狀況。這種狀況發生時,不只讓人困惑,並且也"污染"了高層API。
  爲了不這個問題,高層實現應該捕獲低層的異常,同時拋出一個能夠按照高層抽象進行介紹的異常。這種作法被稱爲"異常轉譯(exception translation)"。

  例如,在Java的集合框架AbstractSequentialList的get()方法以下(基於JDK1.7.0_40):

public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

  listIterator(index)會 返回ListIterator對象,調用該對象的next()方法可能會拋出NoSuchElementException異常。而在get()方法中, 拋出NoSuchElementException異常會讓人感到困惑。因此,get()對NoSuchElementException進行了捕獲,並 拋出了IndexOutOfBoundsException異常。即,至關於將NoSuchElementException轉譯成了 IndexOutOfBoundsException異常。

第6條: 每一個方法拋出的異常都要有文檔

  要單獨的聲明被檢查的異常,而且利用Javadoc的@throws標記,準確地記錄下每一個異常被拋出的條件。
  若是一個類中的許多方法處於一樣的緣由而拋出同一個異常,那麼在該類的文檔註釋中對這個異常作文檔,而不是爲每一個方法單獨作文檔,這是能夠接受的。

第7條: 在細節消息中包含失敗 -- 捕獲消息

  簡而言之,當咱們自定義異常或者拋出異常時,應該包含失敗相關的信息。
  當一個程序因爲一個未被捕獲的異常而失敗的時候,系統會自動打印出該異常的棧軌跡。在棧軌跡中包含該異常的字符串表示。典型狀況下它包含該異常類的類名,以及緊隨其後的細節消息。

第8條: 努力使失敗保持原子性

  當一個對象拋出一個異常以後,咱們總指望這個對象仍然保持在一種定義良好的可用狀態之中。對於被檢查的異常而言,這尤其重要,由於調用者一般指望從被檢查的異常中恢復過來。
  通常而言,一個失敗的方法調用應該保持使對象保持在"它在被調用以前的狀態"。具備這種屬性的方法被稱爲具備"失敗原子性(failure atomic)"。能夠理解爲,失敗了還保持着原子性。對象保持"失敗原子性"的方式有幾種:

(01) 設計一個非可變對象。
(02) 對於在可變對象上執行操做的方法,得到"失敗原子性"的最多見方法是,在執行操做以前檢查參數的有效性。以下(Stack.java中的pop方法):

public Object pop() {
    if (size==0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

(03) 與上一種方法相似,能夠對計算處理過程調整順序,使得任何可能會失敗的計算部分都發生在對象狀態被修改以前。
(04) 編寫一段恢復代碼,由它來解釋操做過程當中發生的失敗,以及使對象回滾到操做開始以前的狀態上。
(05) 在對象的一份臨時拷貝上執行操做,當操做完成以後再把臨時拷貝中的結果複製給原來的對象。
雖然"保持對象的失敗原子性"是指望目標,但它並不老是能夠作獲得。例如,若是多個線程企圖在沒有適當的同步機制的狀況下,併發的訪問一個對象,那麼該對象就有可能被留在不一致的狀態中。

  即便在能夠實現"失敗原子性"的場合,它也不是總被指望的。對於某些操做,它會顯著的增長開銷或者複雜性。
  總的規則是:做爲方法規範的一部分,任何一個異常都不該該改變對象調用該方法以前的狀態,若是這條規則被違反,則API文檔中應該清楚的指明對象將會處於什麼樣的狀態。

第9條: 不要忽略異常

  當一個API的設計者聲明一個方法會拋出某個異常的時候,他們正在試圖說明某些事情。因此,請不要忽略它!忽略異常的代碼以下:

try {
    ...
} catch (SomeException e) {
}

  空的catch塊會使異常達不到應有的目的,異常的目的是強迫你處理不正常的條件。忽略一個異常,就如同忽略一個火警信號同樣 -- 若把火警信號器關閉了,那麼當真正的火災發生時,就沒有人看到火警信號了。因此,至少catch塊應該包含一條說明,用來解釋爲何忽略這個異常是合適 的。

《Java Puzzles》中關於異常的幾個謎題:

謎題1: 優柔寡斷

看看下面的程序,它到底打印什麼?

public class Indecisive {

    public static void main(String[] args) {
        System.out.println(decision());
    }

    private static boolean decision() {
        try {
            return true;
        } finally {
            return false;
        }
    }
}

運行結果

false

結果說明
  在一個 try-finally 語句中,finally 語句塊老是在控制權離開 try 語句塊時執行的。不管 try 語句塊是正常結束的,仍是意外結束的, 狀況都是如此。
  一條語句或一個語句塊在它拋出了一個異常,或者對某個封閉型語句執行了一個 break 或 continue,或是象這個程序同樣在方法中執行了一個return 時,將發生意外結束。它們之因此被稱爲意外結束,是由於它們阻止程序去按順序執行下面的語句。當 try 語句塊和 finally 語句塊都意外結束時, try 語句塊中引起意外結束的緣由將被丟棄, 而整個 try-finally 語句意外結束的緣由將於 finally 語句塊意外結束的緣由相同。在這個程序中,在 try 語句塊中的 return 語句所引起的意外結束將被丟棄, try-finally 語句意外結束是由 finally 語句塊中的 return 而形成的。

  簡單地講, 程序嘗試着 (try) (return) 返回 true, 可是它最終 (finally) 返回(return)的是 false。丟棄意外結束的緣由幾乎永遠都不是你想要的行爲, 由於意外結束的最初緣由可能對程序的行爲來講會顯得更重要。對於那些在 try 語句塊中執行 break、continue 或 return 語句,只是爲了使其行爲被 finally 語句塊所否決掉的程序,要理解其行爲是特別困難的。總之,每個 finally 語句塊都應該正常結束,除非拋出的是不受檢查的異常。 千萬不要用一個 return、break、continue 或 throw 來退出一個 finally 語句塊,而且千萬不要容許將一個受檢查的異常傳播到一個 finally 語句塊以外去。對於語言設計者, 也許應該要求 finally 語句塊在未出現不受檢查的異常時必須正常結束。朝着這個目標,try-finally 結構將要求 finally 語句塊能夠正常結束。return、break 或 continue 語句把控制權傳遞到 finally 語句塊以外應該是被禁止的, 任何能夠引起將被檢查異常傳播到 finally 語句塊以外的語句也一樣應該是被禁止的。

謎題2: 極端難以想象

下面的三個程序每個都會打印些什麼? 不要假設它們均可以經過編譯。
第一個程序

import java.io.IOException;

public class Arcane1 {

    public static void main(String[] args) {
        try {
            System.out.println("Hello world");
        } catch(IOException e) {
            System.out.println("I've never seen println fail!");
        }
    }
}

第二個程序

public class Arcane2 {
    public static void main(String[] args) {
        try {
            // If you have nothing nice to say, say nothing
        } catch(Exception e) {
            System.out.println("This can't happen");
        }
    }
}

第三個程序

interface Type1 {
    void f() throws CloneNotSupportedException;
}

interface Type2 {
    void f() throws InterruptedException;
}

interface Type3 extends Type1, Type2 {
}

public class Arcane3 implements Type3 {
    public void f() {
        System.out.println("Hello world");
    }
    public static void main(String[] args) {
        Type3 t3 = new Arcane3();
        t3.f();
    }
}

運行結果
(01) 第一個程序編譯出錯!

Arcane1.java:9: exception java.io.IOException is never thrown in body of corresponding try statement
        } catch(IOException e) {
          ^
1 error

(02) 第二個程序能正常編譯和運行。
(03) 第三個程序能正常編譯和運行。輸出結果是: Hello world

結果說明
(01) Arcane1展現了被檢查異常的一個基本原則。它看起來應該是能夠編譯的:try 子句執行 I/O,而且 catch 子句捕獲 IOException 異常。可是這個程序不能編譯,由於 println 方法沒有聲明會拋出任何被檢查異常,而IOException 卻正是一個被檢查異常。語言規範中描述道:若是一個 catch 子句要捕獲一個類型爲 E 的被檢查異常, 而其相對應的 try 子句不能拋出 E 的某種子類型的異常,那麼這就是一個編譯期錯誤。

(02) 基於一樣的理由,第二個程序,Arcane2,看起來應該是不能夠編譯的,可是它卻能夠。它之因此能夠編譯,是由於它惟一的 catch 子句檢查了 Exception。儘管在這一點上十分含混不清,可是捕獲 Exception 或 Throwble 的 catch 子句是合法的,無論與其相對應的 try 子句的內容爲什麼。儘管 Arcane2 是一個合法的程序,可是 catch 子句的內容永遠的不會被執行,這個程序什麼都不會打印。

(03) 第三個程序,Arcane3,看起來它也不能編譯。方法 f 在 Type1 接口中聲明要拋出被檢查異常 CloneNotSupportedException,而且在 Type2 接口中聲明要拋出被檢查異常 InterruptedException。Type3 接口繼承了 Type1 和 Type2,所以, 看起來在靜態類型爲 Type3 的對象上調用方法 f 時, 有潛在可能會拋出這些異常。一個方法必需要麼捕獲其方法體能夠拋出的全部被檢查異常, 要麼聲明它將拋出這些異常。Arcane3 的 main 方法在靜態類型爲 Type3 的對象上調用了方法 f,但它對 CloneNotSupportedException 和 InterruptedExceptioin 並無做這些處理。那麼,爲何這個程序能夠編譯呢?
  上述分析的缺陷在於對「Type3.f 能夠拋出在 Type1.f 上聲明的異常和在 Type2.f 上聲明的異常」所作的假設。這並不正確,由於每個接口都限制了方法 f 能夠拋出的被檢查異常集合。一個方法能夠拋出的被檢查異常集合是它所適用的全部類型聲明要拋出的被檢查異常集合的交集,而不是合集。所以,靜態類型爲 Type3 的對象上的 f 方法根本就不能拋出任何被檢查異常。所以,Arcane3能夠毫無錯誤地經過編譯,而且打印 Hello world。

謎題3: 不受歡迎的賓客

下面的程序會打印出什麼呢?

public class UnwelcomeGuest {
    public static final long GUEST_USER_ID = -1;
    private static final long USER_ID;

    static {
        try {
            USER_ID = getUserIdFromEnvironment();
        } catch (IdUnavailableException e) {
            USER_ID = GUEST_USER_ID;
            System.out.println("Logging in as guest");
        }
    }

    private static long getUserIdFromEnvironment() 
        throws IdUnavailableException {
        throw new IdUnavailableException();
    }

    public static void main(String[] args) {
        System.out.println("User ID: " + USER_ID);
    }
}

class IdUnavailableException extends Exception {
}

運行結果

UnwelcomeGuest.java:10: variable USER_ID might already have been assigned
            USER_ID = GUEST_USER_ID;
            ^
1 error

結果說明
  該程序看起來很直觀。對 getUserIdFromEnvironment 的調用將拋出一個異常, 從而使程序將 GUEST_USER_ID(-1L)賦值給 USER_ID, 並打印 Loggin in as guest。 而後 main 方法執行,使程序打印 User ID: -1。表象再次欺騙了咱們,該程序並不能編譯。若是你嘗試着去編譯它, 你將看到和一條錯誤信息。

  問題出在哪裏了?USER_ID 域是一個空 final(blank final),它是一個在聲明中沒有進行初始化操做的 final 域。很明顯,只有在對 USER_ID 賦值失敗時,纔會在 try 語句塊中拋出異常,所以,在 catch 語句塊中賦值是相 當安全的。無論怎樣執行靜態初始化操做語句塊,只會對 USER_ID 賦值一次,這正是空 final 所要求的。爲何編譯器不知道這些呢? 要肯定一個程序是否能夠不止一次地對一個空 final 進行賦值是一個很困難的問題。事實上,這是不可能的。這等價於經典的停機問題,它一般被認爲是不可能解決的。爲了可以編寫出一個編譯器,語言規範在這一點上採用了保守的方式。在程序中,一個空 final 域只有在它是明確未賦過值的地方纔能夠被賦值。規範長篇大論,對此術語提供了一個準確的但保守的定義。 由於它是保守的,因此編譯器必須拒絕某些能夠證實是安全的程序。這個謎題就展現了這樣的一個程序。幸運的是, 你沒必要爲了編寫 Java 程序而去學習那些駭人的用於明確賦值的細節。一般明確賦值規則不會有任何妨礙。若是碰巧你編寫了一個真的可能會對一個空final 賦值超過一次的程序,編譯器會幫你指出的。只有在極少的狀況下,就像本謎題同樣, 你纔會編寫出一個安全的程序, 可是它並不知足規範的形式化要求。編譯器的抱怨就好像是你編寫了一個不安全的程序同樣,並且你必須修改你的程序以知足它。

  解決這類問題的最好方式就是將這個煩人的域從空 final 類型改變爲普通的final 類型,用一個靜態域的初始化操做替換掉靜態的初始化語句塊。實現這一點的最佳方式是重構靜態語句塊中的代碼爲一個助手方法:

public class UnwelcomeGuest {
    public static final long GUEST_USER_ID = -1;
    private static final long USER_ID = getUserIdOrGuest();
    private static long getUserIdOrGuest() {
        try {
            return getUserIdFromEnvironment();
        } catch (IdUnavailableException e) {
            System.out.println("Logging in as guest");
            return GUEST_USER_ID;
        }
    }

    private static long getUserIdFromEnvironment() 
        throws IdUnavailableException {
        throw new IdUnavailableException();
    }

    public static void main(String[] args) {
        System.out.println("User ID: " + USER_ID);
    }
}

class IdUnavailableException extends Exception {
}

  程序的這個版本很顯然是正確的,並且比最初的版本根據可讀性,由於它爲了域值的計算而增長了一個描述性的名字, 而最初的版本只有一個匿名的靜態初始化操做語句塊。將這樣的修改做用於程序,它就能夠如咱們的指望來運行了。總之,大多數程序員都不須要學習明確賦值規則的細節。該規則的做爲一般都是正確的。若是你必須重構一個程序,以消除由明確賦值規則所引起的錯誤,那麼你應該考慮添加一個新方法。這樣作除了能夠解決明確賦值問題,還可使程序的可讀性提升。

謎題4: 您好,再見!

下面的程序將會打印出什麼呢?

public class HelloGoodbye {
    public static void main(String[] args) {
        try {
            System.out.println("Hello world");
            System.exit(0);
        } finally {
            System.out.println("Goodbye world");
        }
    }
}

運行結果:

Hello world

結果說明
  這個程序包含兩個 println 語句: 一個在 try 語句塊中, 另外一個在相應的 finally語句塊中。try 語句塊執行它的 println 語句,而且經過調用 System.exit 來提早結束執行。在此時,你可能但願控制權會轉交給 finally 語句塊。然而,若是你運行該程序,就會發現它永遠不會說再見:它只打印了 Hello world。這是否違背了"Indecisive示例" 中所解釋的原則呢? 不論 try 語句塊的執行是正常地仍是意外地結束, finally 語句塊確實都會執行。然而在這個程序中,try 語句塊根本就沒有結束其執行過程。System.exit 方法將中止當前線程和全部其餘當場死亡的線程。finally 子句的出現並不能給予線程繼續去執行的特殊權限。
  當 System.exit 被調用時,虛擬機在關閉前要執行兩項清理工做。首先,它執行全部的關閉掛鉤操做,這些掛鉤已經註冊到了 Runtime.addShutdownHook 上。這對於釋放 VM 以外的資源將頗有幫助。務必要爲那些必須在 VM 退出以前發生的行爲關閉掛鉤。下面的程序版本示範了這種技術,它能夠如咱們所指望地打印出 Hello world 和 Goodbye world:

public class HelloGoodbye1 {
    public static void main(String[] args) {
        System.out.println("Hello world");
        Runtime.getRuntime().addShutdownHook(
        new Thread() {
            public void run() {
            System.out.println("Goodbye world");
            }
        });
        System.exit(0);
    }
}

  VM 執行在 System.exit 被調用時執行的第二個清理任務與終結器有關。若是System.runFinalizerOnExit 或它的魔鬼雙胞胎 Runtime.runFinalizersOnExit被調用了,那麼 VM 將在全部還未終結的對象上面調用終結器。這些方法好久之前就已通過時了,並且其緣由也很合理。不管什麼緣由,永遠不要調用System.runFinalizersOnExit 和 Runtime.runFinalizersOnExit: 它們屬於 Java類庫中最危險的方法之一[ThreadStop]。調用這些方法致使的結果是,終結器會在那些其餘線程正在併發操做的對象上面運行, 從而致使不肯定的行爲或致使死鎖。

  總之,System.exit 將當即中止全部的程序線程,它並不會使 finally 語句塊獲得調用,可是它在中止 VM 以前會執行關閉掛鉤操做。當 VM 被關閉時,請使用關閉掛鉤來終止外部資源。經過調用 System.halt 能夠在不執行關閉掛鉤的狀況下中止 VM,可是這個方法不多使用。

謎題5: 不情願的構造器

下面的程序將打印出什麼呢?

public class Reluctant {
    private Reluctant internalInstance = new Reluctant();
    public Reluctant() throws Exception {
        throw new Exception("I'm not coming out");
    }
    public static void main(String[] args) {
        try {
            Reluctant b = new Reluctant();
            System.out.println("Surprise!");
        } catch (Exception ex) {
            System.out.println("I told you so");
        }
    }
}

運行結果

Exception in thread "main" java.lang.StackOverflowError
    at Reluctant.<init>(Reluctant.java:3)
    ...

結果說明
  main 方法調用了 Reluctant 構造器,它將拋出一個異常。你可能指望 catch 子句可以捕獲這個異常,而且打印 I told you so。湊近仔細看看這個程序就會發現,Reluctant 實例還包含第二個內部實例,它的構造器也會拋出一個異常。不管拋出哪個異常,看起來 main 中的 catch 子句都應該捕獲它,所以預測該程序將打印 I told you 應該是一個安全的賭注。可是當你嘗試着去運行它時,就會發現它壓根沒有去作這類的事情:它拋出了 StackOverflowError 異常,爲何呢?

  與大多數拋出 StackOverflowError 異常的程序同樣,本程序也包含了一個無限遞歸。當你調用一個構造器時,實例變量的初始化操做將先於構造器的程序體而運行[JLS 12.5]。在本謎題中, internalInstance 變量的初始化操做遞歸調用了構造器,而該構造器經過再次調用 Reluctant 構造器而初始化該變量本身的 internalInstance 域,如此無限遞歸下去。這些遞歸調用在構造器程序體得到執行機會以前就會拋出 StackOverflowError 異常,由於 StackOverflowError 是 Error 的子類型而不是 Exception 的子類型,因此 catch 子句沒法捕獲它。對於一個對象包含與它本身類型相同的實例的狀況,並很多見。例如,連接列表節點、樹節點和圖節點都屬於這種狀況。你必須很是當心地初始化這樣的包含實例,以免 StackOverflowError 異常。

  至於本謎題名義上的題目:聲明將拋出異常的構造器,你須要注意,構造器必須聲明其實例初始化操做會拋出的全部被檢查異常。

謎題6: 域和流

下面的方法將一個文件拷貝到另外一個文件,而且被設計爲要關閉它所建立的每個流,即便它碰到 I/O 錯誤也要如此。遺憾的是,它並不是老是可以作到這一點。爲何不能呢,你如何才能訂正它呢?

static void copy(String src, String dest) throws IOException {
    InputStream in = null;
    OutputStream out = null;
    try {
        in = new FileInputStream(src);
        out = new FileOutputStream(dest);
        byte[] buf = new byte[1024];
        int n;
        while ((n = in.read(buf)) > 0)
            out.write(buf, 0, n);
    } finally {
        if (in != null) in.close();
        if (out != null) out.close();
    }
}

謎題分析
  這個程序看起來已經面面俱到了。其流域(in 和 out)被初始化爲 null,而且新的流一旦被建立,它們立刻就被設置爲這些流域的新值。對於這些域所引用的流,若是不爲空,則 finally 語句塊會將其關閉。即使在拷貝操做引起了一個 IOException 的狀況下,finally 語句塊也會在方法返回以前執行。出什麼錯了呢?
  問題在 finally 語句塊自身中。close 方法也可能會拋出 IOException 異常。若是這正好發生在 in.close 被調用之時,那麼這個異常就會阻止 out.close 被調用,從而使輸出流仍保持在開放狀態。請注意,該程序違反了"優柔寡斷" 的建議:對 close 的調用可能會致使 finally 語句塊意外結束。遺憾的是,編譯器並不能幫助你發現此問題,由於 close 方法拋出的異常與 read 和 write 拋出的異常類型相同,而其外圍方法(copy)聲明將傳播該異常。解決方式是將每個 close 都包裝在一個嵌套的 try 語句塊中。

下面的 finally 語句塊的版本能夠保證在兩個流上都會調用 close:

try {
    // 和以前同樣
} finally {
    if (in != null) {
        try {
            in.close();
        } catch (IOException ex) {
            // There is nothing we can do if close fails
        }
    }

    if (out != null) {
        try {
            out.close();
        } catch (IOException ex) {
            // There is nothing we can do if close fails
        }
    }
}

  總之,當你在 finally 語句塊中調用 close 方法時,要用一個嵌套的 try-catch 語句來保護它,以防止 IOException 的傳播。更通常地講,對於任何在 finally 語句塊中可能會拋出的被檢查異常都要進行處理,而不是任其傳播。

謎題7: 異常爲循環而拋

下面的程序會打印出什麼呢?

public class Loop {
    public static void main(String[] args) {
        int[][] tests = { { 6, 5, 4, 3, 2, 1 }, { 1, 2 },
            { 1, 2, 3 }, { 1, 2, 3, 4 }, { 1 } };
        int successCount = 0;
        try {
            int i = 0;
            while (true) {
                if (thirdElementIsThree(tests[i++]))
                    successCount ++;
            }
        } catch(ArrayIndexOutOfBoundsException e) {
            // No more tests to process
        }
        System.out.println(successCount);
    }
    private static boolean thirdElementIsThree(int[] a) {
        return a.length >= 3 & a[2] == 3;
    }
}

運行結果

0

結果說明
  該程序主要說明了兩個問題。

  第1個問題:不該該使用異常做爲終止循環的手段!
  該程序用 thirdElementIsThree 方法測試了 tests 數組中的每個元素。遍歷這個數組的循環顯然是非傳統的循環:它不是在循環變量等於數組長度的時候終止,而是在它試圖訪問一個並不在數組中的元素時終止。儘管它是非傳統的,可是這個循環應該能夠工做。
  若是傳遞給 thirdElementIsThree 的參數具備 3 個或更多的元素,而且其第三個元素等於 3,那麼該方法將返回 true。對於 tests中的 5 個元素來講,有 2 個將返回 true,所以看起來該程序應該打印 2。若是你運行它,就會發現它打印的時 0。確定是哪裏出了問題,你能肯定嗎? 事實上,這個程序犯了兩個錯誤。第一個錯誤是該程序使用了一種可怕的循環慣用法,該慣用法依賴的是對數組的訪問會拋出異常。這種慣用法不只難以閱讀, 並且運行速度還很是地慢。不要使用異常來進行循環控制;應該只爲異常條件而使用異常。爲了糾正這個錯誤,能夠將整個 try-finally 語句塊替換爲循環遍歷數組的標準慣用法:

for (int i = 0; i < test.length; i++)
    if (thirdElementIsThree(tests[i]))
        successCount++;

若是你使用的是 5.0 或者是更新的版本,那麼你能夠用 for 循環結構來代替:

for (int[] test : tests)
    if(thirdElementIsThree(test))
        successCount++;

  第2個問題: 主要比較"&操做符" 和 "&&操做符"的區別。注意示例中的操做符是&,這是按位進行"與"操做。

相關文章
相關標籤/搜索