Java 異常處理總結(jié)

Java 異常機(jī)制

Java 異常分為檢查異常和非檢查異常,所有RuntimeException的子類都是非檢查異常,其他異常為檢查異常,如下圖所示:


image

Java程序必須顯示處理檢查異常:當(dāng)前方法知道如何處理該異常,則用try...catch塊來處理該異常,否則需要在方法定義時(shí)使用throws關(guān)鍵字聲明拋出該異常。Java程序無須顯式的處理非檢查異常,這類異常通常交由缺省的異常處理代碼處理,通常非檢查異常都和編碼錯(cuò)誤有關(guān)。

本文主要參考了 "Effective Java"一書中關(guān)于異常處理的章節(jié),并結(jié)合AOSP 電子郵件應(yīng)用源碼, 總結(jié)了異常處理的一些規(guī)則。

選擇合適的異常

檢查異常通常用于可以恢復(fù)的場(chǎng)景,如網(wǎng)絡(luò)錯(cuò)誤。在Email登陸的代碼中,如果用戶名密碼有誤,則會(huì)拋出一個(gè)檢查異常,上層代碼捕獲到這個(gè)異常后,可以增加相應(yīng)提示,用戶可根據(jù)提示檢查輸入。相關(guān)代碼如下:

@Override
public synchronized void open(OpenMode mode) throws MessagingException {
    try {
        executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
        executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
    } catch (MessagingException me) {
        if (DebugUtils.DEBUG) {
            LogUtils.d(Logging.LOG_TAG, me.toString());
        }
        throw new AuthenticationFailedException(null, me);
    }
}

非檢查異常,通常用于編碼錯(cuò)誤,這類異常發(fā)生后,通常沒有好的辦法恢復(fù)到執(zhí)行前的狀態(tài)。Email本地執(zhí)行本地搜索功能可選擇標(biāo)題,正文,發(fā)件人,收件人,以及全部五種搜索方式,代碼中搜索方式以一個(gè)整形表示。用于本地搜索的接口會(huì)檢查輸入的搜索方式,如果輸入了未定義的搜索方式,則拋出IllegalArgumentException,相關(guān)代碼如下:

private Cursor uiLocalSearch(Uri uri, String[] projection, boolean unseenOnly){
    if ((approach & SearchParams.SEARCH_BY_RECIVER) == 0
        && (approach & SearchParams.SEARCH_BY_SENDER) == 0
        && (approach & SearchParams.SEARCH_BY_SUBJECT) == 0
        && (approach & SearchParams.SEARCH_BY_CONTENT) == 0) {
        throw new IllegalArgumentException("Invalid search approach: " + approach + " in search query");
    }
}

JDK中定義了很多標(biāo)準(zhǔn)的異常,使用這類異??梢员阌诰S護(hù)代碼的同事理解:

Exception Usage
IllegalArgumentException 輸入?yún)?shù)不合法
IllegalStateException 調(diào)用方法時(shí)對(duì)象狀態(tài)有誤,如未初始化
NullPointerExceptio 輸入了空的參數(shù)(參數(shù)不允許為空)
IndexOutOfBoundsException 輸入的Index值超出范圍
UnsupportedOperationException 尚未實(shí)現(xiàn)的功能被錯(cuò)誤調(diào)用

僅異常情況下使用異常

Java異常機(jī)制主要用于處理異常情況和編碼錯(cuò)誤,不應(yīng)該作業(yè)務(wù)邏輯實(shí)現(xiàn)的手段。下面的代碼使用異常處理檢查空指針,編碼中應(yīng)該要盡量避免這種寫法,例如下面這段代碼:

//不推薦的寫法,應(yīng)盡量避免
String uriAddress = getRingtoneAddressFromUri(context, uri);
if (uriAddress != null) {
    try {
        File file = new File(uriAddress.toString());
        if(file.exists()) {
            return false;
        } else {
            return true;
        }
    } catch (NullPointerException e) {
        return true;
    }
} else {
    return true;
}

上面的代碼缺陷如下:

  • 包含在try…catch內(nèi)的代碼塊通常情況執(zhí)行相對(duì)較慢
  • 由于捕獲了NullPointerException, try…catch調(diào)用的接口實(shí)現(xiàn)中如果包含空指針相關(guān)的編碼錯(cuò)誤,會(huì)被一并捕獲而不是出現(xiàn)Java Crash,造成問題調(diào)試?yán)щy
  • 代碼冗長(zhǎng),難以理解

可以將上述代碼重構(gòu)為:

String uriAddress = getRingtoneAddressFromUri(context, uri);
File file = (uriAddress == null) ? null : new File(uriAddress.toString());
return file == null || !file.exists();
  • 不要忽略異常
    忽略異常平常代碼中很常見,最常見的一種寫法就是將代碼包含在try…catch模塊中,并且catch部分留空:
//不推薦的寫法,應(yīng)盡量避免
try {
    // do something
} catch (SomeException e) {
    // do nothing
}

忽略代碼最直接的影響就是調(diào)試Bug的時(shí)候非常困難,尤其是偶現(xiàn)問題。Email原生代碼在保存附件時(shí),有如下代碼:

//不推薦的寫法,應(yīng)盡量避免
public static String saveAttachment(Context context, InputStream in, Attachment attachment,final boolean updateDb) {
    // do something
    try {
        File file = Utility.createUniqueFile(downloads, attachment.mFileName);
        // do something
    } catch (IOException e) {
       cv.put(AttachmentColumns.UI_STATE,UIProvider.AttachmentState.FAILED);
    }
  // do something 
}

捕獲了IOException又未作任何處理。當(dāng)由于try 中的代碼拋出IOException,導(dǎo)致Bug時(shí),無法從LOG中定位錯(cuò)誤原因。InputStream和OutputSream的關(guān)閉操作通??梢院雎話伋龅腎OException,一般情況下并不會(huì)帶來負(fù)面的影響。在大多數(shù)情況下都不應(yīng)該忽略異常,建議至少增加一條包含調(diào)用棧的打印。

  • 避免拋出不必要的檢查異常
    由于Java程序必須顯示處理檢查異常,如果函數(shù)定義時(shí)聲明了拋出非檢查異常,所有調(diào)用這個(gè)函數(shù)的地方都需要將異常繼續(xù)拋出或者處理這個(gè)異常。一方面這種機(jī)制可以強(qiáng)制工程師在使用接口時(shí)處理異常,另一方面也會(huì)增加接口使用者的負(fù)擔(dān),尤其是當(dāng)一個(gè)方法早期版本不拋出異常,一次修改后增加了異常拋出,此時(shí)需要在之前所有調(diào)用的地方增加異常捕獲代碼。Email中用于獲得DeviceId的函數(shù)聲明拋出IOException就很好的污染了代碼。接口定義如下:
//不推薦的寫法,應(yīng)盡量避免
static public synchronized String getDeviceId(Context context) throws IOException {
    if (sDeviceId == null) {
        sDeviceId = getDeviceIdInternal(context);
    }
    return sDeviceId;
}

在整個(gè)代碼中,有多個(gè)地方調(diào)用到了這個(gè)接口,這些代碼基本上如下:

try {
    return Device.getDeviceId(mContext);
} catch (IOException e) {
    return null;
}

try {
    deviceId = Device.getDeviceId(getActivity());
} catch (IOException e) {
    // Not required
}

可以看出來,這些調(diào)用基本是無意義的。通常來說一個(gè)方法是否有必要拋出檢查異??梢耘袛嘞旅鎯蓚€(gè)條件:

  • 異常情況是否可以通過合理使用方法來避免,比如增加測(cè)試方法是否可行的接口
  • 在方法內(nèi)部是否可以合理的處理發(fā)生的異常

基于上述兩點(diǎn),getDeviceId可以重構(gòu)如下:

//1 增加 測(cè)試接口
static public synchronized boolean isDeviceIdReady() {
    //return true or false
}
//使用API時(shí) :
String deviceId = isDeviceIdReady(): Device.getDeviceId() : null;
//2 方法內(nèi)部處理異常
static public synchronized String getDeviceId(Context context) {
    if (sDeviceId == null) {
        try {
            sDeviceId = getDeviceIdInternal(context);
        } catch (IOException e) {
            if (DEBUG) {
                Log.d("Email", "IOException while getDeviceId", e);
            }
            return "";
        }
    }
    return sDeviceId;
}

不要捕獲所有異常

有時(shí)調(diào)用的一個(gè)方法會(huì)檢測(cè)拋出多個(gè)異常,為了代碼簡(jiǎn)潔,會(huì)捕獲所有異常,如下述代碼:

//不推薦的寫法,應(yīng)盡量避免
try {
    // do someting that might throw IOException
    // do someting that might throw CertificateException
} catch (Exception e) {

}

這樣些會(huì)可能帶來一些問題:

  • 調(diào)用的接口的潛在Bug被忽略
  • 不同類型的異常可能需要不同的處理

上面的代碼可以更改為:

try {
    // do someting that might throw IOException
    // do someting that might throw CertificateException
} catch (IOException e) {
    // Do something handle IOException
} catch (CertificateException e) {
    // Do something handle CertificateException
}

如果所有異常處理方式一致,且JDK版本高于1.7,可以簡(jiǎn)化如下:

try {
    // do someting that might throw IOException
    // do someting that might throw CertificateException
} catch (IOException | CertificateException e) {
    // Do something handle these exception
}

根據(jù)業(yè)務(wù)邏輯抽象異常

有時(shí)某些函數(shù)拋出異常和業(yè)務(wù)代碼的關(guān)系并不明確或是相同業(yè)務(wù)邏輯下需要處理不同的異常,這時(shí)可以通過“異常翻譯”的方法,將這類異常抽象成和業(yè)務(wù)代碼相關(guān)的異常。
以Email登陸認(rèn)證為例,Email登陸不同協(xié)議時(shí)和服務(wù)器的交互方法差別很大,Pop3/Imap協(xié)議采用了Socket,Exchange協(xié)議使用的是http,各協(xié)議下都可能使用SSL, Exchange網(wǎng)絡(luò)交互的代碼和用戶界面的代碼分別運(yùn)行在不同進(jìn)程等等。為了減少用戶界面代碼和具體協(xié)議之間的耦合,不同協(xié)議的登陸代碼都進(jìn)行了必要的“異常翻譯”.

IMAP登陸代碼如下:

public Bundle checkSettings() throws MessagingException {
    int result = MessagingException.NO_ERROR;
    Bundle bundle = new Bundle();
    ImapConnection connection = new ImapConnection(this);
    try {
        connection.open();
        connection.close();
    } catch (IOException ioe) {
        bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage());
        result = MessagingException.IOERROR;
    } finally {
        connection.destroyResponses();
    }
    bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
    return bundle;
}

void open() throws IOException, MessagingException {
    try {
        //excute connection.open();
    } catch (SSLException e) {
        //class CertificateValidationException extends MessagingException
        throw new CertificateValidationException(e.getMessage(), e);
    } catch (IOException ioe) {
        throw ioe;
    } finally {
        destroyResponses();
    }
}

上面的代碼中,Imap相關(guān)代碼將SSLException轉(zhuǎn)換成了MessagingException的子類。

Exchange代碼登陸如下:

public Bundle checkSettings() throws MessagingException {
    try {
        IEmailService svc = getService();        
    if (svc instanceof EmailServiceProxy) {
            ((EmailServiceProxy)svc).setTimeout(90);
        }
        HostAuthCompat hostAuthCom = new HostAuthCompat(mHostAuth);
        return svc.validate(hostAuthCom);
    } catch (RemoteException e) {
        throw new MessagingException("Call to validate generated an exception", e);
    }
}

由于Exchange服務(wù)運(yùn)行在獨(dú)立的進(jìn)程,這里將RemoteException轉(zhuǎn)換成了MessagingException。
有了上面的抽象,用戶界面的代碼就只需要關(guān)注MessagingException即可:

@Override
protected MessagingException doInBackground(Void... params) {
    try {
        //do something
        if ((mMode & SetupDataFragment.CHECK_INCOMING) != 0) {
            final Store store = Store.getInstance(mAccount, mContext);
            final Bundle bundle = store.checkSettings();
        }
        //do something
        return null;
    } catch (final MessagingException me) {
        return me;
    }
}

@Override
protected void onPostExecute(MessagingException result) {
    int progressState = STATE_CHECK_ERROR;
    final int exceptionType = result.getExceptionType();

    switch (exceptionType) {
        // handle the exception
    }
}

如果在后續(xù)代碼中又要增加其他的認(rèn)證方式,只需要實(shí)現(xiàn)新的Store的子類,并抽象可能遇到的異常即可。

  • 拋出異常時(shí)包含足夠的信息
    當(dāng)程序發(fā)生異常時(shí),例如Android開發(fā)中很常見的Java Crash,日志信息中一般都會(huì)包含出現(xiàn)異常時(shí)的調(diào)用棧,以及異常信息toString()方法返回的信息。在拋出異常時(shí),應(yīng)盡可能包含足夠多的信息。以IndexOutOfBoundsException為例:

java.lang.IndexOutOfBoundsException: Index: 3, Size: 3

發(fā)生異常時(shí)打印出了Index值以及數(shù)組的長(zhǎng)度,出現(xiàn)問題時(shí)便于排查。

EmailProvider代碼中的findMatch 也是一個(gè)很好的例子:

private static int findMatch(Uri uri, String methodName) {
    int match = sURIMatcher.match(uri);
    if (match < 0) {
        throw new IllegalArgumentException("Unknown uri: " + uri);
    } else if (Logging.LOGD) {
        LogUtils.v(TAG, methodName + ": uri=" + uri + ", match is " + match);
    }
    return match;
}
  • 原子性的處理異常
    當(dāng)某個(gè)對(duì)象拋出異常時(shí),理想情況下這個(gè)對(duì)象應(yīng)該仍然處于一個(gè)可以穩(wěn)定的可以使用的狀態(tài)。尤其對(duì)于檢查型異常,使用對(duì)象的代碼通常捕獲異常后可以嘗試恢復(fù)措施。一個(gè)簡(jiǎn)單的辦法是,調(diào)用一個(gè)方法發(fā)生異常時(shí),相關(guān)的對(duì)象應(yīng)該初一方法調(diào)用之前的狀態(tài)。下面是一個(gè)例子:
public Object pop() {
    if (size == 0) {
        throw new IllegalStateException("Stuck is empty");
    }
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

如果沒有throw new IllegalStateException("Stuck is empty"),語句當(dāng)棧為空時(shí),執(zhí)行到Object result = elements[--size]會(huì)拋出IndexOutOfBoundsException,但是此時(shí) size已經(jīng)變?yōu)?1,整個(gè)棧 已經(jīng)無法進(jìn)行后續(xù)的操作(比如POP)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容