詳解Java中重要的錯(cuò)誤處理機(jī)制異常
本文轉(zhuǎn)載自微信公眾號「我是開發(fā)者FTD」,作者FTD。轉(zhuǎn)載本文請聯(lián)系我是開發(fā)者FTD公眾號。
hi,大家好,我是開發(fā)者FTD。今天我們來聊一聊Java語言中的異常處理機(jī)制。
Java 語言誕生于1995年,距現(xiàn)在已經(jīng)有26年的時(shí)間了。作為一門比較老的語言依然擁有強(qiáng)大的生命力,Java在很多方面(例如高并發(fā),移植性等)具有明顯的優(yōu)勢,當(dāng)然在一些方面(例如圖像處理)也有不足,今天要給大家介紹的異常就是Java語言中提供的一個(gè)強(qiáng)大的,可以讓我們正確合理的應(yīng)對程序中發(fā)生錯(cuò)誤的機(jī)制。
一、異常介紹
什么是異常?
異常是指程序在運(yùn)行過程中發(fā)生的,由于外部問題導(dǎo)致的程序運(yùn)行異常事件,異常的發(fā)生往往會(huì)中斷程序的運(yùn)行。在 Java 這種面向?qū)ο蟮木幊陶Z言中,萬物都是對象,異常本身也是一個(gè)對象,程序發(fā)生異常就會(huì)產(chǎn)生一個(gè)異常對象。
異常的分類
講到異常的分類,就不能不說一下Java異常的繼承結(jié)構(gòu)。如下圖所示:
Throwable
從圖中可以看到,異常主要有以下類構(gòu)成:
- Throwable
- Error
- Exception
接下來我們就分別介紹一下這幾個(gè)基類的作用。
Throwable
Throwable 類是 Java 語言中所有錯(cuò)誤或異常的頂層父類,其他異常類都繼承于該類。Throwable類有兩個(gè)重要的子類:**Exception(異常)**和 「Error(錯(cuò)誤)」,二者都是 Java 異常處理的重要子類,各自都包含大量子類。
只有當(dāng)對象是此類或其子類的實(shí)例時(shí),才能通過 Java 虛擬機(jī)或者 Java throw 語句拋出。類似地,只有此類或其子類才可以是 catch 子句中的參數(shù)類型。
Throwable 對象中包含了其線程創(chuàng)建時(shí)線程執(zhí)行堆棧的快照,它還包含了給出有關(guān)錯(cuò)誤更多信息的消息字符串。
最后,它還可以包含 cause(原因):另一個(gè)導(dǎo)致此 throwable 拋出的 throwable。此 cause 設(shè)施在 1.4 版本中首次出現(xiàn)。它也稱為異常鏈設(shè)施,因?yàn)?cause 自身也會(huì)有 cause,依此類推,就形成了異常鏈,每個(gè)異常都是由另一個(gè)異常引起的。
Error
Error 是 Throwable 的子類,通常情況下應(yīng)用程序「不應(yīng)該試圖捕獲的嚴(yán)重問題」。
Error 是程序無法處理的錯(cuò)誤,表示運(yùn)行應(yīng)用程序中較嚴(yán)重問題。大多數(shù)錯(cuò)誤與代碼編寫者執(zhí)行的操作無關(guān),而表示代碼運(yùn)行時(shí) JVM(Java 虛擬機(jī))出現(xiàn)的問題。
例如:Java虛擬機(jī)運(yùn)行錯(cuò)誤(Virtual MachineError),當(dāng) JVM 不再有繼續(xù)執(zhí)行操作所需的內(nèi)存資源時(shí),將出現(xiàn) OutOfMemoryError。這些異常發(fā)生時(shí),Java虛擬機(jī)(JVM)一般會(huì)選擇線程終止。
這些錯(cuò)誤表示故障發(fā)生于虛擬機(jī)自身、或者發(fā)生在虛擬機(jī)試圖執(zhí)行應(yīng)用時(shí),如Java虛擬機(jī)運(yùn)行錯(cuò)誤(Virtual MachineError)、類定義錯(cuò)誤(NoClassDefFoundError)等。這些錯(cuò)誤是不可查的,因?yàn)樗鼈冊趹?yīng)用程序的控制和處理能力之 外,而且絕大多數(shù)是程序運(yùn)行時(shí)不允許出現(xiàn)的狀況。對于設(shè)計(jì)合理的應(yīng)用程序來說,即使確實(shí)發(fā)生了錯(cuò)誤,本質(zhì)上也不應(yīng)該試圖去處理它所引起的異常狀況。在 Java中,錯(cuò)誤通過Error的子類描述。
Exception
Exception以及它的子類,代表程序運(yùn)行時(shí)發(fā)送的各種不期望發(fā)生的事件??梢员籎ava異常處理機(jī)制使用,是異常處理的核心。
Exception 異常主要分為兩類:
「1、非檢查性異常(unchecked exception)」
Error 和 RuntimeException 以及他們的子類。Java語言在編譯時(shí),不會(huì)提示和發(fā)現(xiàn)這樣的異常,不要求在程序中處理這些異常。所以我們可以在程序中編寫代碼來處理(使用try…catch…finally)這樣的異常,也可以不做任何處理。對于這些錯(cuò)誤或異常,我們應(yīng)該修正代碼,而不是去通過異常處理器處理。這樣的異常發(fā)生的原因多半是由于我們的代碼邏輯出現(xiàn)了問題。
例如:
- 當(dāng)程序中用數(shù)字除以0時(shí),就會(huì)拋出ArithmeticException異常;
- 在類型轉(zhuǎn)換時(shí),錯(cuò)誤的強(qiáng)制類型轉(zhuǎn)換會(huì)拋出ClassCastException類型轉(zhuǎn)換異常;
- 當(dāng)使用集合進(jìn)行數(shù)組索引越界時(shí)就會(huì)拋出ArrayIndexOutOfBoundsException異常;
- 當(dāng)程序中使用了空對象進(jìn)行操作時(shí)就會(huì)拋出注明的空指針NullPointerException異常等。
「常見的非檢查性異常有」:
異常 | 描述 |
---|---|
ArithmeticException | 當(dāng)出現(xiàn)異常的運(yùn)算條件時(shí),拋出異常。例如,一個(gè)整數(shù)“除以零”時(shí),拋出此類的一個(gè)實(shí)例。 |
ArrayIndexOutOfBoundsException | 用非法索引訪問數(shù)組時(shí)跑出的異常。如果索引為負(fù)或大于等于數(shù)組大小,則該索引為非法索引。 |
ArrayStoreException | 試圖將錯(cuò)誤類型的對象存儲到一個(gè)對象數(shù)組時(shí),拋出的異常。 |
ClassCastException | 試圖將對象強(qiáng)制轉(zhuǎn)換為不是同一個(gè)類型或其子類的實(shí)例時(shí),拋出的異常。 |
IllegalArgumentException | 當(dāng)向一個(gè)方法傳遞非法或不正確的參數(shù)時(shí),拋出該異常。 |
IllegalMonitorStateException | 當(dāng)某一線程已經(jīng)試圖等待對象的監(jiān)視器,或者通知其他正在等待該對象監(jiān)視器的線程,而該線程本身沒有獲得指定監(jiān)視器時(shí)拋出該異常。 |
IllegalStateException | 在非法或不適當(dāng)?shù)臅r(shí)間調(diào)用方法時(shí)產(chǎn)生的信號?;蛘哒fJava環(huán)境或應(yīng)用程序沒有處于請求操作所要求的適當(dāng)狀態(tài)下。 |
IllegalThreadStateException | 線程沒有處于請求操作所要求的適當(dāng)狀態(tài)時(shí),拋出該異常。 |
IndexOutOfBoundsException | 當(dāng)某種排序的索引超出范圍時(shí)拋出的異常,例如,一個(gè)數(shù)組,字符串或一個(gè)向量的排序等。 |
NegativeArraySizeException | 如果應(yīng)用程序試圖創(chuàng)建大小為負(fù)的數(shù)組時(shí),拋出該異常。 |
NullPointerException | 當(dāng)應(yīng)用程序在需要操作對象的時(shí)候而獲得的對象實(shí)例是null時(shí)拋出該異常。 |
NumberFormatException | 當(dāng)應(yīng)用程序試圖將字符串轉(zhuǎn)換成一種數(shù)值類型,但該字符串不能轉(zhuǎn)換為適當(dāng)格式時(shí),拋出該異常。 |
SecurityException | 由安全管理器拋出的異常,指示存在安全侵犯。 |
StringIndexOutOfBoundsException | 此異常由String方法拋出,說明索引為負(fù)或者超出了字符串的大小。 |
「2、檢查性異常(checked exception)」
除了Error 和 RuntimeException的其它異常。Java語言強(qiáng)制要求程序員為這樣的異常做預(yù)備處理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch語句捕獲它并處理,要么用throws子句聲明拋出它,否則編譯不會(huì)通過。這樣的異常一般是由程序的運(yùn)行環(huán)境導(dǎo)致的。因?yàn)槌绦蚩赡鼙贿\(yùn)行在各種未知的環(huán)境下,而程序員無法干預(yù)用戶如何使用他編寫的程序,于是程序員就應(yīng)該為這樣的異常時(shí)刻準(zhǔn)備著。如SQLException,IOException,ClassNotFoundException 等。
檢查性異常就是指,編譯器在編譯期間要求必須得到處理的那些異常,你必須在編譯期處理了。
「常見的檢查性異常有」:
異常 | 描述 |
---|---|
ClassNotFoundException | 當(dāng)應(yīng)用程序試圖加載一個(gè)類,通過名字查找時(shí)卻發(fā)現(xiàn)沒有該類的定義時(shí),拋出該異常。 |
CloneNotSupportedException | 當(dāng)去克隆一個(gè)對象時(shí),發(fā)現(xiàn)該對象沒有實(shí)現(xiàn)Cloneable接口時(shí),拋出該異常。 |
IllegalAccessException | 當(dāng)應(yīng)用程序嘗試通過反射的方式來訪問類、成員變量或調(diào)用方法時(shí),卻無法訪問這些類、成員變量或方法的定義時(shí),拋出該異常。 |
InstantiationException | 當(dāng)試圖使用Class類中的newInstance方法創(chuàng)建一個(gè)類的實(shí)例,而制定的類對象因?yàn)槭且粋€(gè)接口或是一個(gè)抽象類而無法實(shí)例化時(shí),拋出該異常。 |
InterruptedException | 一個(gè)線程被另一個(gè)線程中斷時(shí),拋出該異常。 |
NoSuchFieldException | 當(dāng)找不到指定的變量字段時(shí),拋出該異常、 |
NoSuchMethodException | 當(dāng)找不到指定的類方法時(shí),拋出該異常。 |
二、初識異常
下面我們通過一個(gè)簡單實(shí)例,讓大家更直觀的認(rèn)識一下Java的異常。
下面的代碼會(huì)拋出著名的空指針異常:NullPointerException。
- public class Test {
- private int a = 1;
- private int b = 2;
- public static void main(String[] args) {
- Test t1 = new Test();
- Test t2 = null;
- System.out.println(t1.a);
- System.out.println(t2.a);
- System.out.println(t2.c());
- }
- public String c() {
- return "微信公眾號:我是開發(fā)者FTD";
- }
- }
運(yùn)行程序,控制臺輸出結(jié)果如下:
- 1
- Exception in thread "main" java.lang.NullPointerException
- at cc.devclub.ftd.Test.main(Test.java:11)
- Process finished with exit code 1
從控制臺輸出可以看到,程序打印了 “1”,然后在程序的第11行的位置拋出了 「java.lang.NullPointerException」 ,然后程序就終止運(yùn)行了。
三、異常處理機(jī)制
在編寫代碼處理異常時(shí),對于檢查性異常,有兩種不同的處理方式:
- 使用 「try…catch…finally…」 語句塊處理
- 在方法中使用 「throws/throw」 關(guān)鍵詞將異常交給方法調(diào)用者去處理
try...catch...finally… 關(guān)鍵字
- 使用 try 和 catch 關(guān)鍵字可以捕獲異常。
- try/catch 代碼塊放在異??赡馨l(fā)生的地方。
try/catch代碼塊中的代碼稱為保護(hù)代碼,使用 try/catch 的語法如下:
- try {
- ...
- } catch (IOException ioException) {
- ...
- } catch (Exception exception) {
- ...
- } finally {
- ...
- }
「try 塊:」
- try塊中放可能發(fā)生異常的代碼。
- 如果執(zhí)行完try且不發(fā)生異常,則接著去執(zhí)行finally塊中的代碼和finally后面的代碼(如果有的話)。
- 如果程序發(fā)生異常,則嘗試去匹配對應(yīng)的catch塊。
「catch 塊:」
- 每一個(gè)catch塊用于捕獲并處理一個(gè)特定的異常,或者這異常類型的子類。Java7中可以將多個(gè)異常聲明在一個(gè)catch中。
- catch后面的括號定義了異常類型和異常參數(shù)。如果異常與之匹配且是最先匹配到的,則虛擬機(jī)將使用這個(gè)catch塊來處理異常。
- 在catch塊中可以使用這個(gè)塊的異常參數(shù)來獲取異常的相關(guān)信息。異常參數(shù)是這個(gè)catch塊中的局部變量,其它塊不能訪問。
- 如果當(dāng)前try塊中發(fā)生的異常在后續(xù)的所有catch中都沒捕獲到,則先去執(zhí)行finally,然后到這個(gè)方法的外部調(diào)用者中去匹配異常處理器。
- 如果try中沒有發(fā)生異常,則所有的catch塊將被忽略。
「需要注意的地方」
1、try塊中的局部變量和catch塊中的局部變量(包括異常變量),以及finally中的局部變量,他們之間不可共享使用。
2、每一個(gè)catch塊用于處理一個(gè)異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個(gè)匹配的catch會(huì)得到執(zhí)行。匹配時(shí),不僅運(yùn)行精確匹配,也支持父類匹配,因此,如果同一個(gè)try塊下的多個(gè)catch異常類型有父子關(guān)系,應(yīng)該將子類異常放在前面,父類異常放在后面,這樣保證每個(gè)catch塊都有存在的意義。
3、Java中,異常處理的任務(wù)就是將執(zhí)行控制流從異常發(fā)生的地方轉(zhuǎn)移到能夠處理這種異常的地方去。也就是說:當(dāng)一個(gè)方法的某條語句發(fā)生異常時(shí),這條語句的后面的語句不會(huì)再執(zhí)行,它失去了焦點(diǎn)。執(zhí)行流跳轉(zhuǎn)到最近的匹配的異常處理catch代碼塊去執(zhí)行,異常被處理完后,執(zhí)行流會(huì)接著在“處理了這個(gè)異常的catch代碼塊”后面接著執(zhí)行。
「finally 塊:」
- finally塊不是必須的,通常是可選的。
- 無論異常是否發(fā)生,異常是否匹配被處理,finally中的代碼都會(huì)執(zhí)行。
- 一個(gè)try至少要有一個(gè)catch塊,否則, 至少要有1個(gè)finally塊。但是finally不是用來處理異常的,finally不會(huì)捕獲和處理異常,處理異常的只能是catch塊。
- finally主要做一些清理工作,如流的關(guān)閉,數(shù)據(jù)庫連接的關(guān)閉等。
- finally塊不管異常是否發(fā)生,只要對應(yīng)的try執(zhí)行了,則它一定也執(zhí)行。只有一種方法讓finally塊不執(zhí)行:「System.exit()」 。
大家需要養(yǎng)成**良好的編程習(xí)慣是:**在try塊中打開資源,在finally塊中清理并釋放這些資源,以免造成內(nèi)存泄露。
「需要注意的地方:」
1、在同一try…catch…finally…塊中,如果try中拋出異常,且有匹配的catch塊,則先執(zhí)行catch塊,再執(zhí)行finally塊。如果沒有catch塊匹配,則先執(zhí)行finally,然后去到上層的調(diào)用者中尋找合適的catch塊。
2、在同一try…catch…finally…塊中 ,try發(fā)生異常,且匹配的catch塊中處理異常時(shí)也拋出異常,那么后面的finally也會(huì)執(zhí)行:首先執(zhí)行finally塊,然后去上層調(diào)用者中尋找合適的catch塊。
throws/throw 關(guān)鍵字
- 「throws 關(guān)鍵字」
如果一個(gè)方法內(nèi)部的代碼會(huì)拋出檢查性異常(checked exception),而方法自己又沒有對這些異常完全處理掉,則java的編譯器會(huì)要求你必須在方法的簽名上使用 「throws」 關(guān)鍵字聲明這些可能拋出的異常,否則編譯不通過。
throws 是另一種處理異常的方式,它不同于try…catch…finally…,throws 關(guān)鍵字僅僅是將方法中可能出現(xiàn)的異常向調(diào)用者拋出,而自己則不具體處理。
采取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常,或者說讓調(diào)用者處理更好,調(diào)用者需要為可能發(fā)生的異常負(fù)責(zé)。
- 「throw 關(guān)鍵字」
我們也可以通過 throw 語句手動(dòng)顯式的拋出一個(gè)異常,throw語句的后面必須是一個(gè)異常對象。語法如下:
throw exceptionObject
throw 語句必須寫在方法中,執(zhí)行throw 語句的地方就是一個(gè)異常拋出點(diǎn),它和由JRE自動(dòng)形成的異常拋出點(diǎn)沒有任何差別。
- public void save(User user) {
- if (user == null)
- throw new IllegalArgumentException("User對象為空");
- //......
- }
try-catch-finally 的執(zhí)行順序
try-catch-finally 執(zhí)行順序的相關(guān)問題可以說是各種面試中的「??汀沽?,尤其是 finally 塊中帶有 return 語句的情況。我們直接看幾道面試題:
「面試題一:」
- public static void main(String[] args) {
- int result = test1();
- System.out.println(result);
- }
- public static int test1() {
- int i = 1;
- try {
- i++;
- System.out.println("try block, i = " + i);
- } catch (Exception e) {
- i--;
- System.out.println("catch block i = " + i);
- } finally {
- i = 10;
- System.out.println("finally block i = " + i);
- }
- return i;
- }
大家不妨算一算程序員最終運(yùn)行的結(jié)果是什么。
輸出結(jié)果如下:
- try block, i = 2
- finally block i = 10
- 10
這算一個(gè)相當(dāng)簡單的問題了,沒有坑,下面我們稍微改動(dòng)一下:
- public static int test2() {
- int i = 1;
- try {
- i++;
- throw new Exception();
- } catch (Exception e) {
- i--;
- System.out.println("catch block i = " + i);
- } finally {
- i = 10;
- System.out.println("finally block i = " + i);
- }
- return i;
- }
輸出結(jié)果如下:
- catch block i = 1
- finally block i = 10
- 10
運(yùn)行結(jié)果想必也是意料之中吧,程序拋出一個(gè)異常,然后被本方法的 catch 塊捕獲并進(jìn)行了處理。
「面試題二:」
- public static void main(String[] args) {
- int result = test3();
- System.out.println(result);
- }
- public static int test3() {
- //try 語句塊中有 return 語句時(shí)的整體執(zhí)行順序
- int i = 1;
- try {
- i++;
- System.out.println("try block, i = " + i);
- return i;
- } catch (Exception e) {
- i++;
- System.out.println("catch block i = " + i);
- return i;
- } finally {
- i = 10;
- System.out.println("finally block i = " + i);
- }
- }
輸出結(jié)果如下:
- try block, i = 2
- finally block i = 10
- 2
是不是有點(diǎn)疑惑?明明我 try 語句塊中有 return 語句,可為什么最終還是執(zhí)行了 finally 塊中的代碼?
我們反編譯這個(gè)類,看看這個(gè) test3 方法編譯后的字節(jié)碼的實(shí)現(xiàn):
- 0: iconst_1 //將 1 加載進(jìn)操作數(shù)棧
- 1: istore_0 //將操作數(shù)棧 0 位置的元素存進(jìn)局部變量表
- 2: iinc 0, 1 //將局部變量表 0 位置的元素直接加一(i=2)
- 5: getstatic #3 // 5-27 行執(zhí)行的 println 方法
- 8: new #5
- 11: dup
- 12: invokespecial #6
- 15: ldc #7
- 17: invokevirtual #8
- 20: iload_0
- 21: invokevirtual #9
- 24: invokevirtual #10
- 27: invokevirtual #11
- 30: iload_0 //將局部變量表 0 位置的元素加載進(jìn)操作棧(2)
- 31: istore_1 //把操作棧頂?shù)脑卮嫒刖植孔兞勘砦恢?nbsp;1 處
- 32: bipush 10 //加載一個(gè)常量到操作棧(10)
- 34: istore_0 //將 10 存入局部變量表 0 處
- 35: getstatic #3 //35-57 行執(zhí)行 finally中的println方法
- 38: new #5
- 41: dup
- 42: invokespecial #6
- 45: ldc #12
- 47: invokevirtual #8
- 50: iload_0
- 51: invokevirtual #9
- 54: invokevirtual #10
- 57: invokevirtual #11
- 60: iload_1 //將局部變量表 1 位置的元素加載進(jìn)操作棧(2)
- 61: ireturn //將操作棧頂元素返回(2)
- -------------------try + finally 結(jié)束 ------------
- ------------------下面是 catch + finally,類似的 ------------
- 62: astore_1
- 63: iinc 0, 1
- .......
- .......
從我們的分析中可以看出來,finally 代碼塊中的內(nèi)容始終會(huì)被執(zhí)行,無論程序是否出現(xiàn)異常的原因就是,「編譯器會(huì)將 finally 塊中的代碼復(fù)制兩份并分別添加在 try 和 catch 的后面」。
可能有人會(huì)所疑惑,原本我們的 i 就被存儲在局部變量表 0 位置,而最后 finally 中的代碼也的確將 slot 0 位置填充了數(shù)值 10,可為什么最后程序依然返回的數(shù)值 2 呢?
仔細(xì)看字節(jié)碼,你會(huì)發(fā)現(xiàn)在 return 語句返回之前,虛擬機(jī)會(huì)將待返回的值壓入操作數(shù)棧,等待返回,即使 finally 語句塊對 i 進(jìn)行了修改,但是待返回的值已經(jīng)確實(shí)的存在于操作數(shù)棧中了,所以不會(huì)影響程序返回結(jié)果。
「面試題三:」
- public static int test4() {
- //finally 語句塊中有 return 語句
- int i = 1;
- try {
- i++;
- System.out.println("try block, i = " + i);
- return i;
- } catch (Exception e) {
- i++;
- System.out.println("catch block i = " + i);
- return i;
- } finally {
- i++;
- System.out.println("finally block i = " + i);
- return i;
- }
- }
運(yùn)行結(jié)果:
- try block, i = 2
- finally block i = 3
- 3
其實(shí)你從它的字節(jié)碼指令去看整個(gè)過程,而不要單單死記它的執(zhí)行過程。
你會(huì)發(fā)現(xiàn)程序最終會(huì)采用 finally 代碼塊中的 return 語句進(jìn)行返回,而直接忽略 try 語句塊中的 return 指令。
自定義異常
Java 的異常機(jī)制中所定義的所有異常不可能預(yù)見所有可能出現(xiàn)的錯(cuò)誤,某些特定的情境下,則需要我們自定義異常類型來向上報(bào)告某些錯(cuò)誤信息。
而自定義異常類型也是相當(dāng)簡單的,你可以選擇繼承 Throwable,Exception 或它們的子類,甚至你不需要實(shí)現(xiàn)和重寫父類的任何方法即可完成一個(gè)異常類型的定義。
例如:
- public class MyException extends RuntimeException{ }
- public class MyException extends Exception{ }
按照國際慣例,自定義的異常應(yīng)該總是包含如下的構(gòu)造函數(shù):
- 一個(gè)無參構(gòu)造函數(shù)
- 一個(gè)帶有String參數(shù)的構(gòu)造函數(shù),并傳遞給父類的構(gòu)造函數(shù)。
- 一個(gè)帶有String參數(shù)和Throwable參數(shù),并都傳遞給父類構(gòu)造函數(shù)
- 一個(gè)帶有Throwable 參數(shù)的構(gòu)造函數(shù),并傳遞給父類的構(gòu)造函數(shù)。
下面是IOException類的完整源代碼,我們可以參考:
- public class IOException extends Exception {
- static final long serialVersionUID = 7818375828146090155L;
- public IOException() {
- super();
- }
- public IOException(String message) {
- super(message);
- }
- public IOException(String message, Throwable cause) {
- super(message, cause);
- }
- public IOException(Throwable cause) {
- super(cause);
- }
- }
異常的注意事項(xiàng)
1、當(dāng)子類重寫父類的帶有 throws聲明的函數(shù)時(shí),其throws聲明的異常必須在父類異常的可控范圍內(nèi)——用于處理父類的throws方法的異常處理器,必須也適用于子類的這個(gè)帶throws方法 。這是為了支持多態(tài)。
例如,父類方法throws 的是2個(gè)異常,子類就不能throws 3個(gè)及以上的異常。父類throws IOException,子類就必須throws IOException或者IOException的子類。
2、Java程序可以是多線程的。每一個(gè)線程都是一個(gè)獨(dú)立的執(zhí)行流,獨(dú)立的函數(shù)調(diào)用棧。如果程序只有一個(gè)線程,那么沒有被任何代碼處理的異常 會(huì)導(dǎo)致程序終止。如果是多線程的,那么沒有被任何代碼處理的異常僅僅會(huì)導(dǎo)致異常所在的線程結(jié)束。
也就是說,Java中的異常是線程獨(dú)立的,線程的問題應(yīng)該由線程自己來解決,而不要委托到外部,也不會(huì)直接影響到其它線程的執(zhí)行。
異常使用時(shí)的常見錯(cuò)誤
1、將異常直接顯示在頁面或客戶端
將異常直接打印在客戶端的例子屢見不鮮,一旦程序運(yùn)行出現(xiàn)異常,默認(rèn)情況下容器將異常堆棧信息直接打印在頁面上。從客戶角度來說,任何異常都沒有實(shí)際意義,絕大多數(shù)的客戶也根本看不懂異常信息,軟件開發(fā)也要盡量避免將異常直接呈現(xiàn)給用戶,一定要在前端展示層對異常進(jìn)行封裝后展示。目前絕大多數(shù)應(yīng)用都是前后端分離的模式,這種直接打印異常的情況已經(jīng)相對改善了很多,不過我們在編碼時(shí)還是要特別注意下這個(gè)原則。
2、忽略異常
如下異常處理只是將異常輸出到控制臺,沒有任何意義。而且這里出現(xiàn)了異常并沒有中斷程序,進(jìn)而調(diào)用代碼繼續(xù)執(zhí)行,導(dǎo)致更多的異常。
- public void retrieveObjectById(Long id) {
- try {
- //..some code that throws SQLException
- } catch (SQLException ex) {
- /**
- *了解的人都知道,這里的異常打印毫無意義,僅僅是將錯(cuò)誤堆棧輸出到控制臺。
- * 而在 Production 環(huán)境中,需要將錯(cuò)誤堆棧輸出到日志。
- * 而且這里 catch 處理之后程序繼續(xù)執(zhí)行,會(huì)導(dǎo)致進(jìn)一步的問題*/
- ex.printStacktrace();
- }
- }
捕獲了異常缺不進(jìn)行處理,這是我們在寫代碼時(shí)候的大忌,可以重構(gòu)成:
- public void retrieveObjectById(Long id) {
- try {
- //..some code that throws SQLException
- } catch (SQLException ex) {
- throw new RuntimeException("Exception in retieveObjectById”, ex);
- } finally {
- //clean up resultset, statement, connection etc
- }
- }
3、將異常包含在循環(huán)語句塊中
如下代碼所示,異常包含在 for 循環(huán)語句塊中。
- for (int i = 0; i < 100; i++) {
- try {
- } catch (XXXException e) {
- //....
- }
- }
我們都知道異常處理占用系統(tǒng)資源。一看,大家都認(rèn)為不會(huì)犯這樣的錯(cuò)誤。換個(gè)角度,類 A 中執(zhí)行了一段循環(huán),循環(huán)中調(diào)用了 B 類的方法,B 類中被調(diào)用的方法卻又包含 try-catch 這樣的語句塊。褪去類的層次結(jié)構(gòu),代碼和上面如出一轍。
4、利用 Exception 捕捉所有潛在的異常
一段方法執(zhí)行過程中拋出了幾個(gè)不同類型的異常,為了代碼簡潔,利用基類 Exception 捕捉所有潛在的異常,如下例所示:
- public void retrieveObjectById(Long id) {
- try {
- //...拋出 IOException 的代碼調(diào)用
- //...拋出 SQLException 的代碼調(diào)用
- } catch (Exception e) {
- //這里利用基類 Exception 捕捉的所有潛在的異常,如果多個(gè)層次這樣捕捉,會(huì)丟失原始異常的有效信息
- throw new RuntimeException("Exception in retieveObjectById”, e);
- }
- }
估計(jì)大部分程序員都會(huì)有這種寫法,為了省事簡便,直接一個(gè)頂層的exception來捕獲所有可能出現(xiàn)的異常,這樣雖然可以保證異常肯定會(huì)被捕捉到,但是程序卻無法針對不同的錯(cuò)誤異常進(jìn)行對應(yīng)正確的處理,可以重構(gòu)成:
- public void retrieveObjectById(Long id) {
- try {
- //..some code that throws RuntimeException, IOException, SQLException
- } catch (IOException e) {
- //僅僅捕捉 IOException
- throw new RuntimeException(/*指定這里 IOException 對應(yīng)的錯(cuò)誤代碼*/code, "Exception in retieveObjectById”, e);
- } catch (SQLException e) {
- //僅僅捕捉 SQLException
- throw new RuntimeException(/*指定這里 SQLException 對應(yīng)的錯(cuò)誤代碼*/code, "Exception in retieveObjectById”, e);
- }
- }
5、異常包含的信息不能充分定位問題
異常不僅要能夠讓開發(fā)人員知道哪里出了問題,更多時(shí)候開發(fā)人員還需要知道是什么原因?qū)е碌膯栴},我們知道 java .lang.Exception 有字符串類型參數(shù)的構(gòu)造方法,這個(gè)字符串可以自定義成通俗易懂的提示信息。
簡單的自定義信息開發(fā)人員只能知道哪里出現(xiàn)了異常,但是很多的情況下,開發(fā)人員更需要知道是什么參數(shù)導(dǎo)致了這樣的異常。這個(gè)時(shí)候我們就需要將方法調(diào)用的參數(shù)信息追加到自定義信息中。下例只列舉了一個(gè)參數(shù)的情況,多個(gè)參數(shù)的情況下,可以單獨(dú)寫一個(gè)工具類組織這樣的字符串。
- public void retieveObjectById(Long id) {
- try {
- //..some code that throws SQLException
- } catch (SQLException ex) {
- //將參數(shù)信息添加到異常信息中
- throw new RuntimeException("Exception in retieveObjectById with Object Id :"+ id, ex);
- }
- }
總結(jié)
異常作為Java語言中重要的錯(cuò)誤處理機(jī)制,也作為查找程序原因,提升程序健壯性,前端產(chǎn)品良好體驗(yàn)的重要保障,所以掌握異常的使用是非常有必要的,希望本文能對大家有所幫助,如果有什么疑問或問題歡迎隨時(shí)騷擾。