Java進階之詳談Exception

Daniel_D發表於2018-11-27

寫在最前

最近筆者在撰寫JavaWeb與自動化相結合的教程,上篇入口在這裡,第二篇還在創作中,在釋出之前,讓我們先來討論一個Java的重要技能,Exception。

實現程式的執行是所有初級的程式設計師所追求的,Thinking in Java 因此成為了很適合入門的一本書,然而隨著程式碼行數的累積,越來越多的坑也隨之到來。此時,對基礎知識更深層次的理解就尤為關鍵。在JavaWeb與自動化結合的應用中,無腦丟擲異常會導致程式碼的冗餘與羸弱,今天發的這篇文章將仔細地對Exception的運用進行分析。

需要注意的是,本篇文章並不是對如何丟擲異常的基礎進行講解,需要讀者對Exception機制有一定了解,文中部分用例來自Effective Java,在這裡同時向讀者推薦這本書作為Java進階的重要工具,文末附錄中有筆者Exception部分的英文筆記供大家參考。

使用Exception的情景

不要在類似迭代的迴圈中使用Exception,尤其是涉及ArrayIndexOutOfBounds,如下所示:

try {
    int i = 0;
    while(true)
        array[i++].doSomething();
} catch(ArrayIndexOUtOfBoundsException e) {

}
複製程式碼

主要因為此時使用try-catch有三點顯而易見的壞處:

  • 這樣做違背於JVM設定exception處理的原則,JVM會花費更多的時間來處理。
  • 把Code放在try-catch語句中使得一些JVM執行中的優化被封禁。
  • 規範的迭代寫法是經過優化的,通過JVM的內部處理,避免了很多贅餘的檢查機制,是更合適的選擇。

如果我們在try-catch語句中呼叫了另一個陣列,這個陣列中出現了ArrayIndexOutOfBounds的異常,其中的bug就會被catch exception所矇蔽。相反,標準的迭代寫法會及時的終止執行緒的執行,報出錯誤並且給出追蹤錯誤的路徑讓程式設計師更輕鬆地定位bug的來源。

現在我們通過Java的Iterator介面來看一下標準迭代寫法,在標準的迭代寫法中,我們利用hasNext()作為state-testing判斷方法,來實現state-dependent方法next(),程式碼如下:

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
    //...
}
複製程式碼

綜上所述,Exception是為了異常或者說例外的情況而準備的,不應該在普通的語句中使用,並且程式設計師也不該寫出強迫他人在正常流程的語句中使用Exception的API。

Checked與Unchecked的區別

在Java中Throwable是Exception與Error的父類,而在Effective Java書中,Throwable被分為了以下三類:

  1. Checked Exceptions
  2. Runtime Exceptions
  3. Errors

其中,2和3都是Unchecked Throwable,所以在我們分析Java的異常類時,從Checked與Unchecked兩個邏輯角度來分析會更加清晰。

Checked Exception指那些在編譯過程中會檢查的,這類錯誤在執行中是“可恢復的”。我們需要在寫程式時將其丟擲,換而言之,這些異常應該並不是由程式設計師所導致,而是類似”例行檢查“。

相反,Runtime Exception指的就是程式設計師本身製造出來的錯誤,在文章的第一部分中我們已經明確指出,此類錯誤不應該被丟擲,而應該由程式設計師自己去修復。需要注意的是,一般來說,我們自己設計的Exception應該作為Runtime Exception的直接或者間接子類。如果你對Exception理解得比較淺,暴力地把Runtime Exception的子類背下來,對debug的幫助也相當大,可以快速定位程式碼中的問題。

Errors與Exception不同,他是與JVM相關的,當你在寫演算法時看到棧溢位,那並不是你對語言的理解導致你的程式碼出現漏洞,而是你的資料結構使得JVM出現resource deficiency或invariant failures使得程式無法繼續執行,所以看到Errors的時候,我們也不應將其丟擲,而是應該對程式碼結構進行修改處理。

綜上所述,如果在執行中可恢復,那麼我們就應該將這種Checked Exception丟擲。當不清楚該如何做的時候,丟擲Runtime Exception。重要的是,不要定義既不是Checked Exception子類也不是Runtime Exception子類的Throwable,並且記得在你自定義的Checked Exception中加入方法使程式碼能在執行中恢復。

Checked Exception的使用技巧

我們經常會遇到這種問題,在一個方法中,有一行程式碼需要丟擲Exception,我們需要將他包裹在try-catch語句中。在Java8之後,我們在使用此API時必須丟擲這個異常,這極大地降低了我們程式碼的質量。

解決這個問題最簡單的方法可能就是我們在執行此方法是不返回任何值,但是如果這樣做我們就少了很多通過此方法返回資訊和資料的機會。

因此我們提供了另一種解決方式,那便是通過將需要丟擲Checked Exception的方法拆為兩個方法,使其轉變為一個Unchecked Exception。第一個方法通過返回一個boolean值來指明此Exception是否應該被丟擲,第二個再進行剩餘的操作。下面是一個轉變的簡單例子。

包裹在try-catch中的語句:

try {
    ted.read(book);
} catch (CheckedException e) {
    //...do sth.
}
複製程式碼

下面是改造後的程式碼:

if (ted.understand(book)) {
    ted.read(book);
} else {
    //...do sth.
}
複製程式碼

簡單來說,就是本來是再Ted”讀“這個方法中丟擲他看不懂這個書的異常,但我們將其拆分為”是否理解“與“讀”兩個方法對其進行重構,來避免try-catch的運用。

總的來說,重構Checked Exception是為了程式碼更簡潔更可靠,避免了對Checked Exception的過度使用,因為過度使用會導致API對使用者很不友好。在遇到上面所說的情況時,首先考慮能否使用返回值為空的方法,因為這是最直接最簡單的解決方式。

優先使用標準庫中的Exception

使用Java庫中提供地Exception有三大好處:

  1. 使你的API更容易地被學習與使用,因為大多數程式設計師都瞭解標準的異常
  2. 讓使用了你的API的程式閱讀起來更輕鬆
  3. 更少地佔用記憶體並且更快地對Class進行載入(JVM)

不要直接重用Exception, RuntimeException, Throwable或是Error這些父類,常用的Exception在下表中列出。

Exception 使用場景
IllegalArgumentException 不匹配的非空引數的傳遞
IllegalStateException 未初始化的物件(物件狀態不匹配)
NullPointerException 在未預期的情況下遭遇空指標
IndexOutOfBoundsException 索引引數超出範圍
ConcurrentModificationException 多執行緒對同一個物件進行修改
UnsupportedOperationException 此物件不支援對此方法的引用

需要注意的是,重用的Exception一定要與記錄的語義一致,在文件中詳細說明,並不只是簡單地匹配Exception的名字。

結語

除了上面詳述的幾點外,還要注意的是,首先,每個方法丟擲的異常都要有文件。其次,保持異常的原子性。最重要的是,千萬不要在catch中忽略掉捕獲到的異常

關於異常處理對於很多人來說只是Alt+Enter,但是在程式碼優化階段經常很讓人頭疼,希望本文能使大家有所啟發,對於接下來教程中的一些程式碼有更好的理解,也歡迎大家提問,共同提高。

附錄:Effective Java 讀書筆記

Chapter 10 EXCEPTIONS

Item 69: Use exceptions only for exceptional conditions

Do not use try catch to handle your loop, it might mask the bug and is also very slow.

Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow and do not write APIs that force others to do so.

A well designed API must not force its clients to use exceptions for ordinary control flow.

In iteration codes, one should use hasNext() to decide the life circle of a loop.

Item 70: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

Use checked exceptions for conditions from which the caller can reasonably be expected to recover.

Use runtime exceptions to indicate programming errors.

All of the unchecked throwables you implement should subclass RuntimeException (directly or indirectly).

Don't define any throwables that are neither checked exceptions nor runtime exceptions.

Provide methods on your checked exceptions to aid in recovery.

Item 71: Avoid unnecessary use of checked exceptions

In Java 8, methods throwing checked exceptions can't be used directly in streams.

How to solve the problem that if a method throws a single checked exception, this exception is the sole reason the method must appear in a try block and can't be used directly in streams?

The easiest way to eliminate this is to return an optional of the desired result type.

You can also turn a checked exception into an unchecked exception by breaking the method that throws the exception into two methods, the first of which returns a boolean indicating whether the exception would be thrown.

Item 72: Favor the use of standard exceptions

The Java libraries provide a set of exceptions that covers most of the exceptions-throwing needs of most APIs.

Benefits: makes your API easier to learn because it matches the established conventions, makes programs using your API easier to read, a smaller memory footprint and less time spent loading classes.

Do not reuse Exception, RuntimeException, Throwable, or Error directly.

Reuse must be based on documented semantics, not just on name.

Item 73: Throw exceptions appropriate to the abstraction

Higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction, aka. Exception Translation.

While exception translation is superior to mindless propagation of exceptions from lower layers, it should not be overused.

If it is not feasible to prevent or to handle exceptions from lower layers, use exception translation, unless the lower-level method happens to guarantee that all of its exceptions are appropriate to the higher level.

Item 74: Document all exceptions thrown by each method

Always declare checked exceptions individually, and dovument precisely the conditions under which each one is thrown using @throws tag.

Use the Javadoc @throws tag to document each exception that a method can throw, but do not use the throws keyword on unchecked exceptions.

If an exception is thrown by many methods in a class for the same reason, you can document the exception in the class's documentation comment.

Item 75: Include failure-capture information in detail messages

To capture a failure, the detail message of an exception should contain the values of all parameters and fields that contributed to the exception.

Do not include passwords, encryption keys, and the like in detail messages.

Item 76: Strive for failure atomicity

A failed method invocation should leave the object in the state that it was in prior to the invocation.

Item 77: Don't ignore exceptions

An empty catch block defeats the purpose of exceptions.

If you choose to ignore an exception, the catch block should contain a comment explaining why it is appropriate to do so, and the variable should be named ignored.

相關文章