Java異常的深入研究與分析
1 異常講解
1.1 異常機制概述
異常機制是指當程式出現錯誤後,程式如何處理。具體來說,異常機制提供了程式退出的安全通道。當出現錯誤後,程式執行的流程發生改變,程式的控制權轉移到異常處理器。
1.2 異常處理的流程
當程式中丟擲一個異常後,程式從程式中導致異常的程式碼處跳出,java
虛擬機器檢測尋找和try
關鍵字匹配的處理該異常的catch
塊,如果找到,將控制權交到catch
塊中的程式碼,然後繼續往下執行程式,try
塊中發生異常的程式碼不會被重新執行。如果沒有找到處理該異常的catch
塊,在所有的finally
塊程式碼被執行和當前執行緒的所屬的ThreadGroup
的uncaughtException
方法被呼叫後,遇到異常的當前執行緒被中止。
1.3 異常的結構
異常的繼承結構:Throwable為基類,Error和Exception繼承Throwable,RuntimeException和IOException等繼承Exception。Error和RuntimeException及其子類成為未檢查異常(unchecked),其它異常成為已檢查異常(checked)
1.3.1 Error異常
Error
表示程式在執行期間出現了十分嚴重、不可恢復的錯誤,在這種情況下應用程式只能中止執行,例如JAVA
虛擬機器出現錯誤。Error
是一種unchecked Exception
,編譯器不會檢查Error
是否被處理,在程式中不用捕獲Error
型別的異常。一般情況下,在程式中也不應該丟擲Error
型別的異常。
1.3.2 RuntimeException異常
Exception
異常包括RuntimeException
異常和其他非RuntimeException
的異常。
RuntimeException
是一種Unchecked Exception
,即表示編譯器不會檢查程式是否對RuntimeException
RuntimException
型別的異常,也不必在方法體宣告丟擲RuntimeException
類。RuntimeException
發生的時候,表示程式中出現了程式設計錯誤,所以應該找出錯誤修改程式,而不是去捕獲RuntimeException
。大致有一下幾類:
java.lang.ArrayIndexOutOfBoundsException
:陣列索引越界異常。當對陣列的索引值為負數或大於等於陣列大小時丟擲。java.lang.ArithmeticException
:算術條件異常。譬如:整數除零等。java.lang.NullPointerException
:空指標異常。當應用試圖在要求使用物件的地方使用了null
時,丟擲該異常。譬如:呼叫null
物件的例項方法、訪問null
物件的屬性、計算null
物件的長度、使用throw
語句丟擲null
等等java.lang.NegativeArraySizeException
:陣列長度為負異常java.lang.ArrayStoreException
:陣列中包含不相容的值丟擲的異常java.lang.SecurityException
:安全性異常java.lang.IllegalArgumentException
:非法引數異常
1.3.3 Checked Exception異常
Checked Exception
異常,這也是在程式設計中使用最多的Exception
,所有繼承自Exception
並且不是RuntimeException
的異常都是checked Exception
,上圖中的IOException
和ClassNotFoundException
。JAVA
語言規定必須對checked Exception
作處理,編譯器會對此作檢查,要麼在方法體中宣告丟擲checked Exception
,要麼使用catch語句捕獲checked Exception
進行處理,不然不能通過編譯
比如:
java.lang.ClassNotFoundException
:找不到類異常。當應用試圖根據字串形式的類名構造類,而在遍歷CLASSPAH之後找不到對應名稱的class檔案時,丟擲該異常。
1.4 異常處理
1.4.1 在宣告方法時候丟擲異常
語法:throws(略)
為什麼要在宣告方法丟擲異常?
- 方法是否丟擲異常與方法返回值的型別一樣重要。假設方法丟擲異常卻沒有宣告該方法將丟擲異常,那麼客戶程式設計師可以呼叫這個方法而且不用編寫處理異常的程式碼。那麼,一旦出現異常,那麼這個異常就沒有合適的異常控制器來解決。
為什麼丟擲的異常一定是已檢查異常?
RuntimeException
與Error
可以在任何程式碼中產生,它們不需要由程式設計師顯示的丟擲,一旦出現錯誤,那麼相應的異常會被自動丟擲。遇到Error
,程式設計師一般是無能為力的;遇到RuntimeException
,那麼一定是程式存在邏輯錯誤,要對程式進行修改;只有已檢查異常才是程式設計師所關心的,程式應該且僅應該丟擲或處理已檢查異常。而已檢查異常是由程式設計師丟擲的,這分為兩種情況:客戶程式設計師呼叫會丟擲異常的庫函式;客戶程式設計師自己使用throw語句丟擲異常。
注意
:覆蓋父類某方法的子類方法不能丟擲比父類方法更多的異常,所以,有時設計父類的方法時會宣告丟擲異常,但實際的實現方法的程式碼卻並不丟擲異常,這樣做的目的就是為了方便子類方法覆蓋父類方法時可以丟擲異常。
1.4.2 在方法中如何丟擲異常
語法:throw(略)
丟擲什麼異常?
對於一個異常物件,真正有用的資訊是異常的物件型別,而異常物件本身毫無意義。比如一個異常物件的型別是ClassCastException
,那麼這個類名就是唯一有用的資訊。所以,在選擇丟擲什麼異常時,最關鍵的就是選擇異常的類名能夠明確說明異常情況的類。
異常物件通常有兩種建構函式:一種是無引數的建構函式;另一種是帶一個字串的建構函式,這個字串將作為這個異常物件除了型別名以外的額外說明。
為什麼要建立自己的異常?
當Java
內建的異常都不能明確的說明異常情況的時候,需要建立自己的異常。需要注意的是,唯一有用的就是型別名這個資訊,所以不要在異常類的設計上花費精力。
1.4.3 throw和throws的區別
throw和throws區別:
throw
是語句丟擲一個異常,throws
是方法可能丟擲異常的宣告。(用在宣告方法時,表示該方法可能要丟擲異常)throw
語句用在方法體內,表示丟擲異常,由方法體內的語句處理。throws
語句用在方法聲明後面,表示再丟擲異常,由該方法的呼叫者來處理。throws
主要是宣告這個方法會丟擲這種型別的異常,使它的呼叫者知道要捕獲這個異常。throw
是具體向外拋異常的動作,所以它是丟擲一個異常例項。
基本區別:
throws
是用來宣告一個方法可能
丟擲的所有異常資訊throw
則是指丟擲的一個具體的異常型別。- 通常在一個
方法(類)
的宣告處通過throws
宣告方法(類)可能丟擲的異常資訊,而在方法(類)
內部通過throw
宣告一個具體的異常資訊。 throws
通常不用顯示的捕獲異常,可由系統自動將所有捕獲的異常資訊拋給上級方法;throw
則需要使用者自己捕獲相關的異常,而後在對其進行相關包裝,最後再將包裝後的異常資訊丟擲。
對異常處理方式不同:
throws
對異常不處理,誰呼叫誰處理,throws
的Exception
的取值範圍要大於方法內部異常的最大範圍,而cathch
的範圍又要大於throws
的Exception
的範圍;throw
主動丟擲自定義異常類物件.throws
丟擲的是類,throw
丟擲的是物件.在方法定義中表示的是陳述語氣,第三人稱單數,throw
顯然要加s
。(throws
一般用作方法定義的子句)
在函式體中要用throw
,實際上是祈使句+強調,等價於DO throw ....,do +動詞原形
throw
用於引發異常,可引發預定義異常和自定義異常。
public class TestThrow
{
public static void main(String[] args)
{
try
{
//呼叫帶throws宣告的方法,必須顯式捕獲該異常
//否則,必須在main方法中再次宣告丟擲
throwChecked(-3);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
//呼叫丟擲Runtime異常的方法既可以顯式捕獲該異常,
//也可不理會該異常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception
{
if (a > 0)
{
//自行丟擲Exception異常
//該程式碼必須處於try塊裡,或處於帶throws宣告的方法中
throw new Exception("a的值大於0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if (a > 0)
{
//自行丟擲RuntimeException異常,既可以顯式捕獲該異常
//也可完全不理會該異常,把該異常交給該方法呼叫者處理
throw new RuntimeException("a的值大於0,不符合要求");
}
}
}
補充:throwChecked函式的另外一種寫法如下所示
public static void throwChecked(int a)
{
if (a > 0)
{
//自行丟擲Exception異常
//該程式碼必須處於try塊裡,或處於帶throws宣告的方法中
try
{
throw new Exception("a的值大於0,不符合要求");
}
catch (Exception e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
注意
:此時在main
函式裡面throwChecked
就不用try
異常了。
那麼應該在宣告方法丟擲異常還是在方法中捕獲異常?
處理原則:捕捉並處理哪些知道如何處理的異常,而傳遞哪些不知道如何處理的異常
1.4.4 使用finally塊釋放資源
finally
關鍵字保證無論程式使用任何方式離開try
塊,finally
中的語句都會被執行。在以下三種情況下會進入finally
塊:
- try塊中的程式碼正常執行完畢。
- 在try塊中丟擲異常。
- 在try塊中執行return、break、continue。
因此,當你需要一個地方來執行在任何情況下都必須執行的程式碼時,就可以將這些程式碼放入finally
塊中。當你的程式中使用了外界資源,如資料庫連線,檔案等,必須將釋放這些資源的程式碼寫入finally
塊中。
必須注意的是
:在finally
塊中不能丟擲異常。JAVA
異常處理機制保證無論在任何情況下必須先執行finally
塊然後再離開try
塊,因此在try
塊中發生異常的時候,JAVA
虛擬機器先轉到finally
塊執行finally
塊中的程式碼,finally
塊執行完畢後,再向外丟擲異常。如果在finally
塊中丟擲異常,try
塊捕捉的異常就不能丟擲,外部捕捉到的異常就是finally
塊中的異常資訊,而try
塊中發生的真正的異常堆疊資訊則丟失了。
請看下面的程式碼:
Connection con = null;
try
{
con = dataSource.getConnection();
……
}
catch(SQLException e)
{
……
throw e;//進行一些處理後再將資料庫異常丟擲給呼叫者處理
}
finally
{
try
{
con.close();
}
catch(SQLException e)
{
e.printStackTrace();
……
}
}
執行程式後,呼叫者得到的資訊如下
java.lang.NullPointerException
at myPackage.MyClass.method1(methodl.java:266)
而不是我們期望得到的資料庫異常。這是因為這裡的con
是null的關係,在finally
語句中丟擲了NullPointerException
,在finally
塊中增加對con
是否為null
的判斷可以避免產生這種情況。
丟失的異常
請看下面的程式碼:
public void method2()
{
try
{
……
method1(); //method1進行了資料庫操作
}
catch(SQLException e)
{
……
throw new MyException("發生了資料庫異常:"+e.getMessage);
}
}
public void method3()
{
try
{
method2();
}
catch(MyException e)
{
e.printStackTrace();
……
}
}
上面method2的程式碼中,try
塊捕獲method1
丟擲的資料庫異常SQLException
後,丟擲了新的自定義異常MyException
。這段程式碼是否並沒有什麼問題,但看一下控制檯的輸出:
MyException:發生了資料庫異常:物件名稱'MyTable' 無效。
at MyClass.method2(MyClass.java:232)
at MyClass.method3(MyClass.java:255)
原始異常SQLException
的資訊丟失了,這裡只能看到method2
裡面定義的MyException
的堆疊情況;而method1
中發生的資料庫異常的堆疊則看不到,如何排錯呢,只有在method1
的程式碼行中一行行去尋找資料庫操作語句了。
JDK
的開發者們也意識到了這個情況,Throwable
類增加了兩個構造方法,public Throwable(Throwable cause)
和public Throwable(String message,Throwable cause)
,在建構函式中傳入的原始異常堆疊資訊將會在printStackTrace
方法中打印出來。
1.4.5 try{ return }finally{}中的return
程式碼講解
class Test {
public int aaa() {
int x = 1;
try {
return ++x;
} catch (Exception e) {
} finally {
++x;
}
return x;
}
public static void main(String[] args) {
Test t = new Test();
int y = t.aaa();
System.out.println(y);
}
}
執行結果是2而不是3
- 在
try
語句塊裡使用return
語句,那麼finally
語句塊還會執行
根據已有的知識知道:
return
是可以當作終止語句來用的,我們經常用它來跳出當前方法,並返回一個值給呼叫方法。然後該方法就結束了,不會執行return
下面的語句。
finally
:無論try
語句發生了什麼,無論丟擲異常還是正常執行。finally
語句都會執行。
那麼問題來了。。。。在try語句裡使用return後,finally是否還會執行?finally一定會執行的說法是否還成立?如果成立,那麼先執行return還是先執行finally?
官網解釋:
當
try
語句退出時肯定會執行finally
語句。這確保了即使發了一個意想不到的異常也會執行finally
語句塊。但是finally
的用處不僅是用來處理異常——它可以讓程式設計師不會因為return
、continue
、或者break
語句而忽略了清理程式碼。把清理程式碼放在finally
語句塊裡是一個很好的做法,即便可能不會有異常發生也要這樣做。
注意,當try
或者catch
的程式碼在執行的時候,JVM
退出了。那麼finally
語句塊就不會執行。同樣,如果執行緒在執行try
或者catch
的程式碼時被中斷了或者被殺死了(killed
),那麼finally
語句可能也不會執行了,即使整個運用還會繼續執行。如果
try
語句裡有return
,那麼程式碼的行為如下:
- 如果有返回值,就把返回值儲存到區域性變數中
- 執行jsr指令跳到finally語句裡執行
- 執行完finally語句後,返回之前儲存在區域性變量表裡的值
從上面的官方說明,我們知道無論try
裡執行了return
語句、break
語句、還是continue
語句,finally
語句塊還會繼續執行。同時,在stackoverflow
裡也找到了一個答案,我們可以呼叫System.exit()
來終止它:
finally will be called.
The only time finally won’t be called is: if you call System.exit(), another thread interrupts current one, or if the JVM crashes first.
另外,在java
的語言規範有講到,如果在try
語句裡有return
語句,finally
語句還是會執行。它會在把控制權轉移到該方法的呼叫者或者構造器前執行finally
語句。也就是說,使用return
語句把控制權轉移給其他的方法前會執行finally
語句
其實,此處還牽扯到了值傳遞和引用傳遞關係問題
try
塊:用於捕獲異常。其後可接零個或多個catch
塊,如果沒有catch
塊,則必須跟一個finally
塊。catch
塊:用於處理try
捕獲到的異常。finally
塊:無論是否捕獲或處理異常,finally
塊裡的語句都會被執行。當在try
塊或catch
塊中遇到return
語句時,finally
語句塊將在方法返回之前被執行。在以下4種特殊情況下,finally塊不會被執行:
1)在finally
語句塊中發生了異常。
2)在前面的程式碼中用了System.exit()
退出程式。
3)程式所在的執行緒死亡。
4)關閉CPU。