讓我們來看一段有問題的代碼:
handler.sendMessageDelayed(msg, -1);
這個方法的作用是延遲一段時間之后,發(fā)送一條消息。其中第一個參數(shù)msg是要發(fā)送的消息,第二個參數(shù)是延時的時間,單位是ms,按理來講這個延時的時間必須大于等于0。但是萬一其他人在調(diào)用的時候?qū)⑦@個參數(shù)傳進(jìn)去一個負(fù)數(shù),比如-1,如果是你,你會怎么處理這種情況?
我們的代碼在運(yùn)行的過程當(dāng)中,難免會出現(xiàn)一些錯誤,有開發(fā)人員人為導(dǎo)致的問題,比如除數(shù)為0,傳進(jìn)來一個空指針,或者某個人的年齡被設(shè)置為了負(fù)數(shù),有些是外部原因,比如正在讀取某個文件的時候,這個文件被刪了,或者服務(wù)器給了你一個不存在的鏈接。
采取哪種錯誤處理方式,取決于錯誤的產(chǎn)生原因和嚴(yán)重程度。
采用默認(rèn)值
用我們剛才的例子來舉例,handler.sendMessageDelayed的第二個參數(shù)不能為負(fù)數(shù),但是如果為負(fù)數(shù),源碼當(dāng)中是這么處理的:
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
把小于0的輸入,都當(dāng)做0來處理。這是一種非常實用的處理方式。我們平時就經(jīng)常用到這種方式,只是可能沒有留意而已,比如SharedPreferences:
sharedPreferences.getString("username", "");
在獲取數(shù)據(jù)的時候,如果失敗,則返回一個默認(rèn)值,而不是null,會使得你的程序更加健壯并且更加優(yōu)雅。所以不要把第二個參數(shù)設(shè)置為null,因為如果設(shè)置為null的話,接下來又要有一個是否為null的判斷,白費(fèi)了這個方法的設(shè)計者這樣設(shè)計的苦心。
斷言
Java提供了斷言機(jī)制,采用assert關(guān)鍵字。
assert s != null;
斷言就像是“活的”注釋,它告訴我們程序運(yùn)行到這個地方的時候,應(yīng)該處于什么樣的狀態(tài),或是滿足什么樣的條件。
需要特別說明一下,如果你在Android Studio中使用assert關(guān)鍵字,IDE會建議你這樣使用:
if (BuildConfig.DEBUG && s == null) throw new AssertionError();
因為用assert關(guān)鍵字來斷言有個缺陷,就是無論你是在Debug版本還是Release版本當(dāng)中,這些斷言都無可避免的會被執(zhí)行,而如果我們采用了上面的寫法,在Release版本中,這些斷言就不會存在了。
此外,Google的Guava庫當(dāng)中,也有一些用來起到類似斷言功能的方法,推薦大家使用。
Preconditions.checkNotNull();
Preconditions.checkArgument();
Preconditions.checkElementIndex();
Preconditions.checkState();
Preconditions.checkPositionIndex();
Preconditions.checkPositionIndexes();
運(yùn)行時異常
在這里,有一種情況需要特別注意一下:
Thread thread = new Thread();
thread.start();
thread.start();
我們都知道,一個線程是不能啟動兩次的,如果開發(fā)人員啟動了兩次,這種錯誤該如何處理?
程序員在編程過程當(dāng)中人為導(dǎo)致的錯誤,所以應(yīng)該拋出運(yùn)行時異常。實際上android源碼當(dāng)中是這樣處理這個問題的:
public synchronized void start() {
if (hasBeenStarted) {
throw new IllegalThreadStateException("Thread already started");
}
...
}
針對這類問題,對應(yīng)的解決辦法應(yīng)該是避免多次啟動。
所有的運(yùn)行時異常都應(yīng)該從根源上去避免這個錯誤,而不是用try-catch代碼塊來捕獲這個異常。
忽略
有人可能會這么處理剛才線程被啟動兩次的問題:
public synchronized void start() {
if (hasBeenStarted) {
return;
}
...
}
如果這個線程已經(jīng)被啟動,則返回,什么都不做。這種做法有一定的缺陷,它在一定程度上放任了開發(fā)人員犯錯誤,同時它也有好處,我們來看另外一個例子:
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
...
}
FileOutputStream的close方法就是這么做的,它忽略了多次調(diào)用。如果有兩種情況下,都需要關(guān)閉流,那么這樣可以省掉一些繁瑣的判斷。建議在線程啟動、文件流打開的時候要嚴(yán)格限制只允許調(diào)用一次,而停止或者關(guān)閉的操作允許多次調(diào)用,因為一般停止和關(guān)閉這類操作都不止一處需要調(diào)用,至少有正常關(guān)閉和程序執(zhí)行過程中遇到異常關(guān)閉兩種,所以在關(guān)閉的時候不要限制那么嚴(yán)格也是有好處的。
非運(yùn)行時異常
人們對非運(yùn)行時異常褒貶不一,大家爭論的地方主要在于非運(yùn)行時異常必須得處理,而且它讓代碼變得很難看。一行代碼變成了四行之外,還多了一層縮進(jìn)。結(jié)果就是哪里出現(xiàn)非運(yùn)行時異常,哪里的代碼就會丑爆了。那么何時該使用非運(yùn)行時異常,一般來說,非運(yùn)行時異常都是可以恢復(fù)的。
非運(yùn)行時異常的優(yōu)點
強(qiáng)制程序員去處理出錯的情況。如果不強(qiáng)制大家去處理這個情況,很多人都會選擇不處理異常,比如下面的方法:
file.delete();
這個方法是有一個返回值的,返回true表示刪除成功,false表示刪除失敗,這里,沒有拋出非運(yùn)行時異常,所以很多人都會閉上眼睛假裝不會失敗,導(dǎo)致程序不穩(wěn)定。
非運(yùn)行時異常的缺點
- 那些發(fā)生概率極低或者明知道不會發(fā)生問題的地方,也必須要捕獲異常。
比如下面的代碼:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
這段代碼丑陋的地方在于,這個線程,我根本不打算去執(zhí)行interrupt方法,sleep方法就根本不會拋出異常,catch代碼段根本不可能執(zhí)行到,或者即便拋異常了,也沒什么大不了的,但是我們還是得去捕獲這個異常。Google也發(fā)現(xiàn)了這個問題,所以給了大家另外一個選擇:
SystemClock.sleep(1000);
- 一些設(shè)計的不好的異常,即便捕獲了,也無法處理。
try {
JSONObject jsonObject = new JSONObject("");
} catch (JSONException e) {
throw new RuntimeException(e);
}
比如這里的JSONException,雖然它是非運(yùn)行時異常,但是幾乎不可恢復(fù),所以在catch代碼塊中也就只能再拋出一個運(yùn)行時異常。
總結(jié)
實際工作中遇到的情況各種各樣,但是一個總的原則就是通過分析錯誤的產(chǎn)生原因和嚴(yán)重程度來選擇處理錯誤的方式。