空標志錯誤模式
在我的上一篇文章中,我說明了用空指針代替各種不同基本類型的數據是如何成為引起 NullPointerException 異常最普遍的原因之一的。這一次,我將說明用空指針代替異常情況怎麼也會導致問題的出現。在 Java 程序中,異常情況通常是通過拋出異常,並在適當的控制點捕獲它們來進行處理。但是經常看到的方法是通過返回一個空指針值來表明這種情況(以及,可能打印一條消息到 System.err )。如果調用方法沒有明確地檢查空指針,它可能會嘗試丟棄返回值並觸發一個空指針異常。
您可能會猜想,之所以稱這種模式為空標志錯誤模式,是因為它是不一致地使用空指針作為異常情況的標志引起的。
起因
讓我們來考慮一下下面的這個簡單的橋類(從 BufferedReaders 到 Iterators ):
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.Iterator;
public class BufferedReaderIterator implements Iterator {
private BufferedReader internal;
public BufferedReaderIterator(BufferedReader _internal) {
this.internal = _internal;
}
public boolean hasNext() {
try {
boolean result = true;
// Let's suppose that lines in the underlying input stream are known
// to be no greater than 80 characters long.
internal.mark(80);
if (this.next() == null) {
result = false;
}
internal.reset();
return result;
}
catch (IOException e) {
System.err.println(e.toString());
return false;
}
}
public Object next() {
try {
return internal.readLine();
}
catch (IOException e) {
System.err.println(e.toString());
return null;
}
}
public void remove() {
// This iterator does not support the remove operation.
throw new UnsupportedOperationException();
}
}
因為這個類作為 Iterator 接口的橋接實現,代碼必須從 BufferedReader 捕獲 IOException 異常。每一種方法通過返回某個缺省值來處理 IOException 。對於 hasNext ,返回 false 值。這是合理的,因為如果 IOException 異常被拋出,客戶就不應該指望能從 Iterator 檢索到另一個元素。另一方面,在 IOException 異常(因為它取決於 internal.readLine() 的返回值)和 internal 是空的情況下, next 都返回 null 。但這不是 Iterator 對象的客戶所期待的。正常情況下,在沒有更多元素的 Iterator 上調用 next 時,會拋出一個 NoSuchElementException 異常。如果我們的 Iterator 的客戶依賴於這種行為,它很可能會嘗試丟棄從調用 next 返回的空指針,結果導致 NullPointerException 異常。
不管 NullPointerException 異常什麼時候出現,都要對如上所述的情況作檢查。這種錯誤模式的出現很普遍。
預防措施
盡管這種錯誤模式經常出現,使用空標志仍是非常沒有根據的(與上例的情況一樣)。讓我們來重寫 next ,使它如我們期望的一樣拋出 NoSuchElementException 異常:
public Object next() {
try {
String result = internal.readLine();
if (result == null) {
throw new NoSuchElementException();
}
else {
return result;
}
}
catch (IOException e) {
// The original exception is included in the message to notify the
// client that an IOException has occurred.
throw new NoSuchElementException(e.toString());
}
}
請注意:要使其余的代碼能使用修改過的方法,我們還必須:
導入 java.util.NoSuchElementException 。
修正 hasNext ,使其不再調用 next 來進行測試。最簡單的修正方法是只要直接調用 internal.readLine() 。
另一種處理 IOException 異常的方法是捕獲它們,並代替它們拋出 RuntimeException 異常。決定這樣做是基於對目標平台上預計 IOException 異常出現頻率的估計。如果很頻繁,那麼您可能想試著從中恢復。
調用這個新 next 方法的任何代碼可能都不得不處理拋出的 NoSuchElementException 異常。(當然,代碼可以簡單地選擇忽略它們並允許程序異常終止。)如果這樣,與原始代碼拋出的 NullPointerException 異常相比,產生的錯誤消息和拋出異常的位置所提供的信息要豐富得多。如果拋出的異常是檢查過的異常(比如 IOException ),那麼它會更有用,因為除非處理了異常,否則類的客戶代碼將不編譯。利用這種方法,我們甚至可以在程序運行前排除某些錯誤發生的可能性。但是,在這個示例中,不破壞 Iterator 接口,就不能拋出這樣一個檢查過的異常。因此,為了重復使用在 Iterators 上運行的代碼,我們犧牲了一些靜態檢查。靜態檢查的目的和重復使用的目的之間的這種矛盾是很普遍的。
總結
在我完成這篇文章前,我要提醒許多經常使用空標志的程序員注意。許多程序員會爭辯說這會使他們的程序更“健壯”。畢竟,他們可能會說,健壯的系統能夠適當地處理不同的情況,而不是一遇到小問題就拋出異常。但是這種爭辯忽視了這樣一種事實,即異常是增強代碼健壯性的有力工具,它允許在異常情況下控制能快速傳送到最適合控制的位置。另一方面,空標志的使用把控制流限制在方法調用和返回的普通方式(當然,一直到整個程序崩潰)。此外,這樣使用空標志,程序員有效地掩蓋了異常情況出現位置的跡象。誰知道空指針在被丟棄前從方法到方法傳遞了多遠?這只能使得診斷錯誤以及確定怎樣修正它們更加困難。經驗證明這種代碼經常中斷。我們首要關注的應該是避免這種困惑,使診斷盡可能容易。因此,作為准則,我努力編寫可以盡快通知異常情況的代碼,並且嘗試著僅從沒有指示程序錯誤的異常情況中恢復。
即使在代碼中盡量避免使用空標志,您仍要不可避免地處理使用了空標志的舊代碼。事實上,許多 Java 類庫本身,比如我們上面使用過的 Hashtable 類和 BufferedReader 類都用了空標志。當使用這樣的類時,您可以通過在執行前,顯式檢查操作是否將返回空來避免錯誤。例如,對於 Hashtables ,我總是在調用 get 之前用 containsKey 進行測試。但是,盡管采用這種預防手段,這種錯誤模式仍然是最常碰到的錯誤模式之一。
下面是本周的錯誤模式的小結:
模式:空標志
症狀:使用空指針作為異常情況的標志的代碼塊報告 NullPointerException 異常。
起因:調用方法沒有檢查作為返回值的空指針。
治療和預防措施:拋出異常來報告異常情況。
在下一篇文章中,我將討論與類強制轉換異常有關的錯誤模式。