這篇是要來談談 Guava 的 Throwables,不過正如標題寫的,程式 90% 的比率在管理與處理錯誤,既然如此,那我們還是從頭來認識一些錯誤處理的基礎好了。
假設你有個提款方法原先是這麼寫的:
public boolean withdraw(int money) {
if(money >= 0 && this.balance >= money) {
this.balance -= money;
return true;
}
retrun false;
}
withdraw
在提款成功時會傳回 true
,失敗時傳回 false
,為了要確認提款是否成功,客戶端必須主動檢查傳回值,萬一忘記了,執行時也沒有任何機制可以主動通知客戶端,會有什麼結果無從得知。如果使用的是 Java,可以在執行不成功時拋出(throw)錯誤。例如:
public void withdraw(int money) throws Throwable {
if(money < 0 || this.balance < money) {
throw new Throwable("提款失敗");
}
this.balance -= money;
}
可以搭配 throw
語法的物件,必須是 Throwable
的實例,方法上可以使用 throws
聲明會有 Throwable
實例拋出,這會是方法簽署的一部份,如此客戶端只要查看文件,無需查閱原始碼,就知道該用 try-catch
語法處理,實際上編譯器也會強制客戶端必須處理。不過,參數
money
似乎不應該傳入負數,這一開始就要規範,如果都規範好不準傳入負數,而客戶端還傳入負數,那一定是客戶端的 Bug,客戶端應該停下來修改他的程式;另外,餘額不足是商務上的邏輯考量,這是客戶端可以處理的錯誤,嗯!至少 Java 當初設計時是這麼想的啦!總之,應該可以將 withdraw
設計成:
public void withdraw(int money) throws Exception {
if(money < 0) throw new IllegalArgumentException("提款額不能傳入負數");
if(this.balance < money) {
throw new Exception("餘額不足");
}
this.balance -= money;
}
Java 為 Throwable
定義了兩個 Error
與 Exception
兩個子類別。Error
的實例是指那些發生了你也不能做什麼補救措施的錯誤,Exception
則是 Java 為那些能補救的錯誤而設計的類別,等等!為什麼 IllegalArgumentException
不用使用 throws
宣告在方法上?前面講過,編譯器會強制客戶端要處理例外,除了那些
RuntimeException
及 Error
的實例之外。Error
可以理解,都歸類在不可能有補救措施的錯誤了,編譯器還強迫處理就沒有道理,至於 RuntimeException
,則是用來通知客戶端他的程式有 Bug 了,才導致方法中會拋出例外,IllegalArgumentException
就是一種 RuntimeException
。其實學過 Java 的你一定知道,那些編譯器強制檢查的
Exception
叫 Checked Exception,那些編譯器不強制檢查的 RuntimeException
叫 Unchecked Exception。Java 當初想得很美好,也是唯一有 Checked Exception、Unchecked Exception 分別的語言,不過顯然開發者不領情的多,唔!篇幅有限,有興趣可以看看 貼心還是造成麻煩? 這篇文章。程式中可能會捕捉例外,對例外做些能做的處理,再重新拋出例外,像是 要抓還是要丟? 中提到的,因此,像這樣的程式碼倒蠻常見的:
public void doSomething() throws IOException, SQLException {
try {
someMethodThatCouldThrowAnything();
} catch(IKnowWhatToDoWithThisException e) {
handle(e);
} catch(SQLException e) {
log(e);
throw e;
} catch(IOException e) {
log(e);
throw e;
} catch(Throwable t) {
log(t);
throw new RuntimeException(t);
}
}
最後一個 catch
照單全收,防禦式地處理其他沒考量到的可能錯誤,因為方法上宣告會拋出 IOExcepton
與 SQLException
,為了傳播 Throwable
實例,程式中將之包裹在 RuntimeException
中,編譯器會很高興地忽略它。不過 catch
了 SQLException
及 IOException
的兩個區塊程式碼重複了。如果你使用 JDK7,那可以用新語法改寫為:
public void doSomething() throws IOException, SQLException {
try {
someMethodThatCouldThrowAnything();
} catch(IKnowWhatToDoWithThisException e) {
handle(e);
} catch(SQLException | IOException e) { // JDK7 multi-catch 語法
log(e);
throw e;
} catch(Throwable t) {
log(t);
throw new RuntimeException(t);
}
}
那麼如果是 JDK6 呢?可以使用 Guava 的 Throwables
來改寫為更簡潔的方式:
public void doSomething() throws IOException, SQLException {
try {
someMethodThatCouldThrowAnything();
} catch(IKnowWhatToDoWithThisException e) {
handle(e);
} catch(Throwable t) {
log(t);
Throwables.propagateIfInstanceOf(t, IOException.class);
Throwables.propagateIfInstanceOf(t, SQLException.class);
throw Throwables.propagate(t);
}
}
Throwables.propagateIfInstanceOf
作了什麼事?其實就是將 t
轉型為指定的類型並重新拋出:
...
public static <X extends Throwable> void propagateIfInstanceOf(
@Nullable Throwable throwable, Class<X> declaredType) throws X {
if (throwable != null && declaredType.isInstance(throwable)) {
throw declaredType.cast(throwable);
}
}
...
而 Throwables.propagate
就是將傳入的 t
包裹為 RuntimeException
傳回罷了:
...
public static RuntimeException propagate(Throwable throwable) {
propagateIfPossible(checkNotNull(throwable));
throw new RuntimeException(throwable);
}
...
Guava 的 Throwables
使用上很簡單,你可以再看看 ThrowablesExplained 的說明,應該就清楚多了。我這邊想再多談一下 JDK7 的 multi-catch,在 multi-catch 時,
catch
中的例外型態,不能有上下繼承關係,有沒有想過為什麼呢?catch(IOException | FileNotFoundException e)
或許還可以理解,因為展開為不使用 multi-catch 的寫法的話會變成:
...
} catch(IOException e) {
handle(e);
} catch(FileNotFoundException e) {
handle(e);
}
...
只是為什麼 catch(FileNotFoundException | IOException e) 也不行呢?想想看如果不用 multi-catch 的寫法的話會如何?
...
} catch(FileNotFoundException e) {
handle(e);
} catch(IOException e) {
handle(e);
}
...
好像合理耶!其實是因為,還沒使用 multi-catch 前的寫法本來就沒必要,想想你在 FileNotFoundException
與 IOException
的處理本來就相同,那為什麼不直接寫成以下就好?
...
} catch(IOException e) {
handle(e);
}
...
你知道,Java 的編譯器蠻雞婆的,因此在 catch(FileNotFoundException | IOException e)
時就不讓你過關了。還有一個情況是,有人想在 multi-catch 後,使用 instanceof
判斷是不是某個特定例外型態之實例,像是:
...
} catch(IOException | SQLException e) {
if(e instanceof FileNotFoundException) {
doSomethingWhenFileNotFoundException(e);
}
handle(e);
}
...
這完全是不建議,multi-catch 本來就是讓你可以整理相同的例外處理程式碼,如果你用了 instanceof
針對特定型態做特定處理,就表示這個型態不該待在 multi-catch 中,也就是你沒用 multi-catch 的情況下,應該會像是:
...
} catch(FileNotFoundException e) {
doSomethingWhenFileNotFoundException(e);
} catch(IOException e) {
handle(e);
} catch(SQLException e) {
handle(e);
}
...
那麼用 multi-catch 整理後,應該要是這樣才對:
...
} catch(FileNotFoundException e) {
doSomethingWhenFileNotFoundException(e);
} catch(IOException | SQLException e) {
handle(e);
}
...
這邊想表達的是,語法的簡便,其實只是讓你省去少打幾個字之類的麻煩,不過如果你不知道本質上該如何處理,那麼也是會發生濫用的情況。同樣地,Guava 的 Throwables
看來是省了一些功夫,不過也別亂用, ThrowablesExplained 的說明中,有一些不建議的使用方式,建議你瞭解一下,看看原因,最終其實都是有關於,例外或錯誤該怎麼處理與對待的問題。