一、介紹
在認為編寫的程式中不可能沒有一點錯誤,當程式運作時,發生了編寫者不希望的事件,阻止了程式的正常執行,這就是異常。當異常出現時,我們處理的方式就是本文要介紹的主要内容。
Java的異常處理機制:
在Java中異常被當作對象來處理,其根類為java.lang.Throwable,Throwable又派生出Error類和Exception類。

總體上我們根據Javac對異常的處理要求,将異常類分為2類。
非檢查異常(unckecked exception):Error 和 RuntimeException 以及他們的子類。javac在編譯時,不會提示和發現這樣的異常,不要求在程式處理這些異常。是以如果願意,我們可以編寫代碼處理(使用try...catch...finally)這樣的異常,也可以不處理。對于這些異常,我們應該修正代碼,而不是去通過異常處理器處理 。這樣的異常發生的原因多半是代碼寫的有問題。如除0錯誤ArithmeticException,錯誤的強制類型轉換錯誤ClassCastException,數組索引越界ArrayIndexOutOfBoundsException,使用了空對象NullPointerException等等。
檢查異常(checked exception):除了Error 和 RuntimeException的其它異常。javac強制要求程式員為這樣的異常做預備處理工作(使用try...catch...finally或者throws)。在方法中要麼用try-catch語句捕獲它并處理,要麼用throws子句聲明抛出它,否則編譯不會通過。這樣的異常一般是由程式的運作環境導緻的。因為程式可能被運作在各種未知的環境下,而程式員無法幹預使用者如何使用他編寫的程式,于是程式員就應該為這樣的異常時刻準備着。如SQLException , IOException,ClassNotFoundException 等。
需要明确的是:檢查和非檢查是對于javac來說的,這樣就很好了解和區分了。
二、異常的基本處理方式
先看一個處理異常的例子:
1 public class yichang {
2 public static void main(String[] args) {
3 chufa x = new chufa();
4 x.chufa1(1,0);
5 }
6 }
7 class chufa{
8
9 public int chufa1(int a,int b){
10 int c = 0;
11 try {
12 c = a/b;
13 } catch (Exception e) {
14 e.printStackTrace();
15 }
16 return c;
17 }
18 }
java.lang.ArithmeticException: / by zero
at chufa.chufa1(yichang.java:14)
at yichang.main(yichang.java:6)
這裡是一個十分基礎的異常處理,在try中的語句發生了異常無法正常往下執行的時候,就會執行catch,而catch這裡有一個Exception e,這是一個異常對象,而在catch裡面執行了這個e對象printStackTrace(),這個方法的作用是将異常抛出,讓調用這個方法的地方去處理這個異常。
從輸出可以看出,在try語句中出現異常的時候,catch抛出異常,而抛出這個異常就到了main函數裡(mian函數調用了chufa),是以在輸出的第三行也顯示了mian函數出現異常,這可以了解為異常從出現的地方找到了最近調用它的函數,将異常抛給它,類似于異常的冒泡,一切設計到異常的地方都會被顯示。
那如果這裡我們不用異常捕獲,編譯器并不知道這個地方要發生異常,也不會強制我們添加異常捕獲,因為這個異常是非檢查異常
再看看檢查異常下面例子:
1 public void testException()
2 {
3 //FileInputStream的構造函數會抛出FileNotFoundException
4 FileInputStream fileIn=new FileInputStream("E:\\a.txt");
5
6 int word;
7 //read方法會抛出IOException
8 while((word=fileIn.read())!=-1)
9 {
10 System.out.print((char)word);
11 }
12 //close方法會抛出IOException
13 fileIn.close();
14 }
這段代碼是不能通過編譯的,因為裡面可能會出現檢查異常,編譯器會強制編寫者對異常進行處理
在Java中除了可以用try catch來對異常進行捕獲和處理之外,還有其他兩種方法,共有三種:
1、上文提到try catch
2、可以在方法體外用throws進行抛出聲明,這個抛出會告知調用這個方法的對象,這個方法可能會出現的異常,這裡也有兩種情況:
- 如果抛出的是非處理異常,調用這個方法的對象可以選擇性的進行異常處理,也就是編譯器不會因為不處理這個異常而報錯
- 如果抛出的是處理異常,那調用這個方法的對象就必須顯式的處理這個異常,将其抛出到更高層,或者進行其他操作
3、在代碼塊中用throw手動抛出一個異常對象,此時也會有兩種情況:
- 如果抛出的是處理異常,那調用這個方法的對象就必須顯式的處理這個異常,将其抛出到更高層,或者進行其他操作(與上面一樣)
(如果最終将異常抛給main方法,則相當于交給jvm自動處理,此時jvm會簡單地列印異常資訊)
三、深入認識幾種異常處理的方式
1、Try,catch,finally
這裡又多出一個fianlly關鍵字,try關鍵字可以配合剩餘兩個關鍵字使用,三個關鍵字有幾種組合方式:
(1)try{代碼塊}
catch{代碼塊}
(2)try{代碼塊}
finally{代碼塊}
(3)try{代碼塊}
catch{代碼塊}
finally{代碼塊}
這裡try是用來捕獲異常,catch是當出現異常時執行的操作,finally不管try裡是否出現異常都會執行,catch塊可以有多個,try和finally隻能有一個,同時finally也可以不添加。
當有多個catch塊的時候,是按照catch塊的先後順序進行比對,一旦異常被一個catch塊比對,則不會與後面的catch塊進比對——根據這個我們得出,當一個catch塊能捕捉異常的範圍要大于其他catch塊,大範圍的應該放在後面進行比對,如果把大範圍的放在前面,那範圍小的在後面永遠不可能被比對
那再看如下代碼:
1 class test{
2 public static void main(String[] args) {
3 TestException t = new TestException();
4 String x = t.chufa(1,0);
5 System.out.println(x);
6 }
7 }
8 class TestException{
9 public String chufa(int a,int b){
10 int p = 0;
11 try {
12 p = a/b;
13 } catch (Exception e) {
14 return "This is catch";
15 }finally {
16 return "This is finally";
17 }
18 }
19 }
可以考慮一下這一段代碼的輸出:
This is finally
這三個關鍵字原本的執行順序應該是:
try内代碼塊若沒有出現異常,不會執行catch,直接執行finally
try内代碼塊若出現異常,會先執行catch進行處理異常的操作,然後再執行finally
我們可以看上面的例子,try裡面出現了異常,這裡肯定先執行了catch,但catch裡的語句直接傳回了,按理應該不會執行finally,但最終結果卻是傳回的是finally的傳回值。
從這裡可以看出,無論前面是否有return語句,finally塊的語句都會執行。
我們稍微修改一下上面的例子
1 class x{
2 public static void main(String[] args) {
3 TestException t = new TestException();
4 String x = t.chufa(1,0);
5 System.out.println(x);
6 }
7 }
8 class TestException{
9 public String chufa(int a,int b){
10 int p = 0;
11 try {
12 p = a/b;
13 } catch (Exception e) {
14 System.out.println("到catch");//試驗是否執行catch
15 return "This is catch";
16 }finally {
17 return "This is finally";
18 }
19 }
20 }
到catch
This is finally
這裡我們徹底明白了,catch是先于finally執行,但catch裡要傳回了,編譯器就直接先執行了finally的代碼塊,讓程式直接傳回,如果finally裡面不是傳回值,則程式還是會在catch裡進行傳回
是以finally執行的語句可能會覆寫我們到傳回值,是以千萬不要在finally寫任何傳回語句,finally主要的作用就是進行資源的釋放。
2、throws和thow關鍵字
throws用在方法聲明的地方,表明這個方法可能會抛出某種異常,然後将異常交給調用其到程式來處理,throws後面可以跟着多個異常。
throw是一定會抛出異常,這是當程式執行到某處的時候編寫者主動去抛出特定異常,隻能用在方法體中,而throw一個很重要的作用就是可以進行異常的轉換,可以抛出想抛出的資訊。
3、繼承關系中的異常
本小節讨論子類重寫父類方法的時候,如何确定異常抛出聲明的類型。下面是三點原則:
1)父類的方法沒有聲明異常,子類在重寫該方法的時候不能聲明異常;
2)如果父類的方法聲明一個異常exception1,則子類在重寫該方法的時候聲明的異常不能是exception1的父類;
3)如果父類的方法聲明的異常類型隻有非運作時異常(運作時異常),則子類在重寫該方法的時候聲明的異常也隻能有非運作時異常(運作時異常),不能含有運作時異常(非運作時異常)。
4、異常進行中的建議
以下是根據前人總結的一些異常處理的建議:
1.隻在必要使用異常的地方才使用異常,不要用異常去控制程式的流程
謹慎地使用異常,異常捕獲的代價非常高昂,異常使用過多會嚴重影響程式的性能。如果在程式中能夠用if語句和Boolean變量來進行邏輯判斷,那麼盡量減少異常的使用,進而避免不必要的異常捕獲和處理。比如下面這段經典的程式:
1 public void useExceptionsForFlowControl() {
2 try {
3 while (true) {
4 increaseCount();
5 }
6 } catch (MaximumCountReachedException ex) {
7 }
8 //Continue execution
9 }
10
11 public void increaseCount() throws MaximumCountReachedException {
12 if (count >= 5000)
13 throw new MaximumCountReachedException();
14 }
上邊的useExceptionsForFlowControl()用一個無限循環來增加count直到抛出異常,這種做法并沒有說讓代碼不易讀,而是使得程式執行效率降低
2.切忌使用空catch塊
在捕獲了異常之後什麼都不做,相當于忽略了這個異常。千萬不要使用空的catch塊,空的catch塊意味着你在程式中隐藏了錯誤和異常,并且很可能導緻程式出現不可控的執行結果。如果你非常肯定捕獲到的異常不會以任何方式對程式造成影響,最好用Log日志将該異常進行記錄,以便日後友善更新和維護。
3.檢查異常和非檢查異常的選擇
一旦你決定抛出異常,你就要決定抛出什麼異常。這裡面的主要問題就是抛出檢查異常還是非檢查異常。
檢查異常導緻了太多的try…catch代碼,可能有很多檢查異常對開發人員來說是無法合理地進行處理的,比如SQLException,而開發人員卻不得不去進行try…catch,這樣就會導緻經常出現這樣一種情況:邏輯代碼隻有很少的幾行,而進行異常捕獲和處理的代碼卻有很多行。這樣不僅導緻邏輯代碼閱讀起來晦澀難懂,而且降低了程式的性能。
我個人建議盡量避免檢查異常的使用,如果确實該異常情況的出現很普遍,需要提醒調用者注意處理的話,就使用檢查異常;否則使用非檢查異常。
是以,在一般情況下,我覺得盡量将檢查異常轉變為非檢查異常交給上層處理。
5.不要将提供給使用者看的資訊放在異常資訊裡
1 public class Main {
2 public static void main(String[] args) {
3 try {
4 String user = null;
5 String pwd = null;
6 login(user,pwd);
7 } catch (Exception e) {
8 System.out.println(e.getMessage());
9 }
10
11 }
12
13 public static void login(String user,String pwd) {
14 if(user==null||pwd==null)
15 throw new NullPointerException("使用者名或者密碼為空");
16 //...
17 }
18 }
展示給使用者錯誤提示資訊最好不要跟程式混淆一起,比較好的方式是将所有錯誤提示資訊放在一個配置檔案中統一管理。
6.避免多次在日志資訊中記錄同一個異常
隻在異常最開始發生的地方進行日志資訊記錄。很多情況下異常都是層層向上跑出的,如果在每次向上抛出的時候,都Log到日志系統中,則會導緻無從查找異常發生的根源。
7. 異常處理盡量放在高層進行
盡量将異常統一抛給上層調用者,由上層調用者統一之時如何進行處理。如果在每個出現異常的地方都直接進行處理,會導緻程式異常處理流程混亂,不利于後期維護和異常錯誤排查。由上層統一進行處理會使得整個程式的流程清晰易懂。
8. 在finally中釋放資源
如果有使用檔案讀取、網絡操作以及資料庫操作等,記得在finally中釋放資源。這樣不僅會使得程式占用更少的資源,也會避免不必要的由于資源未釋放而發生的異常情況。
參考資料:
https://www.cnblogs.com/dolphin0520/p/3769804.html
https://www.cnblogs.com/lulipro/p/7504267.html
11