鍍金池/ 教程/ Java/ Throwables:簡(jiǎn)化異常和錯(cuò)誤的傳播與檢查
不可變集合
排序: Guava 強(qiáng)大的”流暢風(fēng)格比較器”
強(qiáng)大的集合工具類(lèi):java.util.Collections 中未包含的集合工具
新集合類(lèi)型
常見(jiàn) Object 方法
I/O
前置條件
字符串處理:分割,連接,填充
散列
原生類(lèi)型
數(shù)學(xué)運(yùn)算
使用和避免 null
Throwables:簡(jiǎn)化異常和錯(cuò)誤的傳播與檢查
google Guava 包的 ListenableFuture 解析
事件總線(xiàn)
緩存
函數(shù)式編程
區(qū)間
集合擴(kuò)展工具類(lèi)
Google-Guava Concurrent 包里的 Service 框架淺析
google Guava 包的 reflection 解析

Throwables:簡(jiǎn)化異常和錯(cuò)誤的傳播與檢查

異常傳播

有時(shí)候,你會(huì)想把捕獲到的異常再次拋出。這種情況通常發(fā)生在 Error 或 RuntimeException 被捕獲的時(shí)候,你沒(méi)想捕獲它們,但是聲明捕獲 Throwable 和 Exception 的時(shí)候,也包括了了 Error 或 RuntimeException。Guava 提供了若干方法,來(lái)判斷異常類(lèi)型并且重新傳播異常。例如:


    try {
        someMethodThatCouldThrowAnything();
    } catch (IKnowWhatToDoWithThisException e) {
        handle(e);
    } catch (Throwable t) {
        Throwables.propagateIfInstanceOf(t, IOException.class);
        Throwables.propagateIfInstanceOf(t, SQLException.class);
        throw Throwables.propagate(t);
    }

所有這些方法都會(huì)自己決定是否要拋出異常,但也能直接拋出方法返回的結(jié)果——例如,throw Throwables.propagate(t);—— 這樣可以向編譯器聲明這里一定會(huì)拋出異常。

Guava 中的異常傳播方法簡(jiǎn)要列舉如下:

RuntimeException propagate(Throwable) 如果 Throwable 是 Error 或 RuntimeException,直接拋出;否則把 Throwable 包裝成 RuntimeException 拋出。返回類(lèi)型是 RuntimeException,所以你可以像上面說(shuō)的那樣寫(xiě)成throw Throwables.propagate(t),Java 編譯器會(huì)意識(shí)到這行代碼保證拋出異常。
void propagateIfInstanceOf( Throwable, Class<X extends Exception>) throws X Throwable 類(lèi)型為 X 才拋出
void propagateIfPossible( Throwable) Throwable 類(lèi)型為 Error 或 RuntimeException 才拋出
void propagateIfPossible( Throwable, Class<X extends Throwable>) throws X Throwable 類(lèi)型為 X, Error 或 RuntimeException 才拋出

Throwables.propagate 的用法

模仿 Java7 的多重異常捕獲和再拋出

通常來(lái)說(shuō),如果調(diào)用者想讓異常傳播到棧頂,他不需要寫(xiě)任何 catch 代碼塊。因?yàn)樗淮蛩銖漠惓V谢謴?fù),他可能就不應(yīng)該記錄異常,或者有其他的動(dòng)作。他可能是想做一些清理工作,但通常來(lái)說(shuō),無(wú)論操作是否成功,清理工作都要進(jìn)行,所以清理工作可能會(huì)放在 finallly 代碼塊中。但有時(shí)候,捕獲異常然后再拋出也是有用的:也許調(diào)用者想要在異常傳播之前統(tǒng)計(jì)失敗的次數(shù),或者有條件地傳播異常。

當(dāng)只對(duì)一種異常進(jìn)行捕獲和再拋出時(shí),代碼可能還是簡(jiǎn)單明了的。但當(dāng)多種異常需要處理時(shí),卻可能變得一團(tuán)糟:


    @Override public void run() {
        try {
            delegate.run();
        } catch (RuntimeException e) {
            failures.increment();
            throw e;
        }catch (Error e) {
            failures.increment();
            throw e;
        }
    }

Java7 用多重捕獲解決了這個(gè)問(wèn)題:


    } catch (RuntimeException | Error e) {
        failures.increment();
        throw e;
    }

非 Java7 用戶(hù)卻受困于這個(gè)問(wèn)題。他們想要寫(xiě)如下代碼來(lái)統(tǒng)計(jì)所有異常,但是編譯器不允許他們拋出 Throwable(譯者注:這種寫(xiě)法把原本是 Erro r或 RuntimeException 類(lèi)型的異常修改成了 Throwable,因此調(diào)用者不得不修改方法簽名):


    } catch (Throwable t) {
        failures.increment();
        throw t;
    }

解決辦法是用 throw Throwables.propagate(t)替換 throw t。在限定情況下(捕獲 Error 和 RuntimeException),Throwables.propagate 和原始代碼有相同行為。然而,用 Throwables.propagate 也很容易寫(xiě)出有其他隱藏行為的代碼。尤其要注意的是,這個(gè)方案只適用于處理 RuntimeException 或 Error。如果 catch 塊捕獲了受檢異常,你需要調(diào)用 propagateIfInstanceOf 來(lái)保留原始代碼的行為,因?yàn)?Throwables.propagate 不能直接傳播受檢異常。

總之,Throwables.propagate 的這種用法也就馬馬虎虎,在 Java7 中就沒(méi)必要這樣做了。在其他 Java 版本中,它可以減少少量的代碼重復(fù),但簡(jiǎn)單地提取方法進(jìn)行重構(gòu)也能做到這一點(diǎn)。此外,使用 propagate 會(huì)意外地包裝受檢異常。

非必要用法:把拋出的 Throwable 轉(zhuǎn)為 Exception

有少數(shù) API,尤其是 Java 反射 API 和(以此為基礎(chǔ)的)Junit,把方法聲明成拋出 Throwable。和這樣的 API 交互太痛苦了,因?yàn)榧词故亲钔ㄓ玫?API 通常也只是聲明拋出 Exception。當(dāng)確定代碼會(huì)拋出 Throwable,而不是 Exception 或 Error 時(shí),調(diào)用者可能會(huì)用 Throwables.propagate 轉(zhuǎn)化 Throwable。這里有個(gè)用 Callable 執(zhí)行 Junit 測(cè)試的范例:


    public Void call() throws Exception {
        try {
            FooTest.super.runTest();
        } catch (Throwable t) {
            Throwables.propagateIfPossible(t, Exception.class);
            Throwables.propagate(t);
        }

        return null;
    }

在這兒沒(méi)必要調(diào)用 propagate()方法,因?yàn)?propagateIfPossible 傳播了 Throwable 之外的所有異常類(lèi)型,第二行的 propagate 就變得完全等價(jià)于 throw new RuntimeException(t)。(題外話(huà):這個(gè)例子也提醒我們,propagateIfPossible 可能也會(huì)引起混亂,因?yàn)樗坏珪?huì)傳播參數(shù)中給定的異常類(lèi)型,還拋出 Error 和 RuntimeException)

這種模式(或類(lèi)似于 throw new RuntimeException(t)的模式)在 Google 代碼庫(kù)中出現(xiàn)了超過(guò) 30 次。(搜索’propagateIfPossible[^;]* Exception.class[)];’)絕大多數(shù)情況下都明確用了”throw new RuntimeException(t)”。我們也曾想過(guò)有個(gè)”throwWrappingWeirdThrowable”方法處理 Throwable 到 Exception 的轉(zhuǎn)化。但考慮到我們用兩行代碼實(shí)現(xiàn)了這個(gè)模式,除非我們也丟棄 propagateIfPossible 方法,不然定義這個(gè) throwWrappingWeirdThrowable 方法也并沒(méi)有太大必要。

Throwables.propagate 的有爭(zhēng)議用法

爭(zhēng)議一:把受檢異常轉(zhuǎn)化為非受檢異常

原則上,非受檢異常代表 bug,而受檢異常表示不可控的問(wèn)題。但在實(shí)際運(yùn)用中,即使 JDK 也有所誤用——如 Object.clone()、Integer. parseInt(String)、URI(String)——或者至少對(duì)某些方法來(lái)說(shuō),沒(méi)有讓每個(gè)人都信服的答案,如 URI.create(String)的異常聲明。

因此,調(diào)用者有時(shí)不得不把受檢異常和非受檢異常做相互轉(zhuǎn)化:


    try {
        return Integer.parseInt(userInput);
    } catch (NumberFormatException e) {
        throw new InvalidInputException(e);
    }

    try {
        return publicInterfaceMethod.invoke();
    } catch (IllegalAccessException e) {
        throw new AssertionError(e);
    }

有時(shí)候,調(diào)用者會(huì)使用 Throwables.propagate 轉(zhuǎn)化異常。這樣做有沒(méi)有什么缺點(diǎn)?最主要的恐怕是代碼的含義不太明顯。throw Throwables.propagate(ioException)做了什么?throw new RuntimeException(ioException)做了什么?這兩者做了同樣的事情,但后者的意思更簡(jiǎn)單直接。前者卻引起了疑問(wèn):”它做了什么?它并不只是把異常包裝進(jìn) RuntimeException 吧?如果它真的只做了包裝,為什么還非得要寫(xiě)個(gè)方法?”。應(yīng)該承認(rèn),這些問(wèn)題部分是因?yàn)椤眕ropagate”的語(yǔ)義太模糊了(用來(lái)拋出未聲明的異常嗎?)。也許”wrapIfChecked”更能清楚地表達(dá)含義。但即使方法叫做”wrapIfChecked”,用它來(lái)包裝一個(gè)已知類(lèi)型的受檢異常也沒(méi)什么優(yōu)點(diǎn)。甚至?xí)衅渌秉c(diǎn):也許比起 RuntimeException,還有更合適的類(lèi)型——如 IllegalArgumentException。 我們有時(shí)也會(huì)看到 propagate 被用于傳播可能為受檢的異常,結(jié)果是代碼相比以前會(huì)稍微簡(jiǎn)短點(diǎn),但也稍微有點(diǎn)不清晰:


    } catch (RuntimeException e) {
        throw e;
    }catch (Exception e) {
        throw new RuntimeException(e);
    }

    } catch (Exception e) {
     throw Throwables.propagate(e);
    }

然而,我們似乎故意忽略了把檢查型異常轉(zhuǎn)化為非檢查型異常的合理性。在某些場(chǎng)景中,這無(wú)疑是正確的做法,但更多時(shí)候它被用于避免處理受檢異常。這讓我們的話(huà)題變成了爭(zhēng)論受檢異常是不是壞主意了,我不想對(duì)此多做敘述。但可以這樣說(shuō),Throwables.propagate 不是為了鼓勵(lì)開(kāi)發(fā)者忽略 IOException 這樣的異常。

爭(zhēng)議二:異常穿隧

但是,如果你要實(shí)現(xiàn)不允許拋出異常的方法呢?有時(shí)候你需要把異常包裝在非受檢異常內(nèi)。這種做法挺好,但我們?cè)俅螐?qiáng)調(diào),沒(méi)必要用 propagate 方法做這種簡(jiǎn)單的包裝。實(shí)際上,手動(dòng)包裝可能更好:如果你手動(dòng)包裝了所有異常(而不僅僅是受檢異常),那你就可以在另一端解包所有異常,并處理極少數(shù)特殊場(chǎng)景。此外,你可能還想把異常包裝成特定的類(lèi)型,而不是像 propagate 這樣統(tǒng)一包裝成 RuntimeException。

爭(zhēng)議三:重新拋出其他線(xiàn)程產(chǎn)生的異常


    try {
        return future.get();
    } catch (ExecutionException e) {
        throw Throwables.propagate(e.getCause());
    }

對(duì)這樣的代碼要考慮很多方面:

  • ExecutionException 的 cause 可能是受檢異常,見(jiàn)上文”爭(zhēng)議一:把檢查型異常轉(zhuǎn)化為非檢查型異?!薄5绻覀兇_定 future 對(duì)應(yīng)的任務(wù)不會(huì)拋出受檢異常呢?(可能 future 表示 runnable 任務(wù)的結(jié)果——譯者注:如 ExecutorService 中的 submit(Runnable task, T result)方法)如上所述,你可以捕獲異常并拋出 AssertionError 。尤其對(duì)于 Future,請(qǐng)考慮 Futures.get 方法。(TODO:對(duì) future.get()拋出的另一個(gè)異常 InterruptedException 作一些說(shuō)明)
  • ExecutionException 的 cause 可能直接是 Throwable 類(lèi)型,而不是 Exception 或 Error。(實(shí)際上這不大可能,但你想直接重新拋出 cause 的話(huà),編譯器會(huì)強(qiáng)迫你考慮這種可能性)見(jiàn)上文”用法二:把拋出 Throwable 改為拋出 Exception”。
  • ExecutionException 的 cause 可能是非受檢異常。如果是這樣的話(huà),cause 會(huì)直接被 Throwables.propagate 拋出。不幸的是,cause 的堆棧信息反映的是異常最初產(chǎn)生的線(xiàn)程,而不是傳播異常的線(xiàn)程。通常來(lái)說(shuō),最好在異常鏈中同時(shí)包含這兩個(gè)線(xiàn)程的堆棧信息,就像 ExecutionException 所做的那樣。(這個(gè)問(wèn)題并不單單和 propagate 方法相關(guān);所有在其他線(xiàn)程中重新拋出異常的代碼都需要考慮這點(diǎn))

異常原因鏈

Guava 提供了如下三個(gè)有用的方法,讓研究異常的原因鏈變得稍微簡(jiǎn)便了,這三個(gè)方法的簽名是不言自明的:

Throwable getRootCause(Throwable)
List<Throwable> getCausalChain(Throwable)
String getStackTraceAsString(Throwable)