您好,登錄后才能下訂單哦!
這篇文章主要介紹“Java中重要的錯誤處理機制異常有哪些”,在日常操作中,相信很多人在Java中重要的錯誤處理機制異常有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java中重要的錯誤處理機制異常有哪些”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
一、異常介紹
什么是異常?
異常是指程序在運行過程中發生的,由于外部問題導致的程序運行異常事件,異常的發生往往會中斷程序的運行。在 Java 這種面向對象的編程語言中,萬物都是對象,異常本身也是一個對象,程序發生異常就會產生一個異常對象。
異常的分類
Throwable
從圖中可以看到,異常主要有以下類構成:
Throwable
Error
Exception
接下來我們就分別介紹一下這幾個基類的作用。
Throwable
Throwable 類是 Java 語言中所有錯誤或異常的頂層父類,其他異常類都繼承于該類。Throwable類有兩個重要的子類:**Exception(異常)**和 「Error(錯誤)」,二者都是 Java 異常處理的重要子類,各自都包含大量子類。
只有當對象是此類或其子類的實例時,才能通過 Java 虛擬機或者 Java throw 語句拋出。類似地,只有此類或其子類才可以是 catch 子句中的參數類型。
Throwable 對象中包含了其線程創建時線程執行堆棧的快照,它還包含了給出有關錯誤更多信息的消息字符串。
最后,它還可以包含 cause(原因):另一個導致此 throwable 拋出的 throwable。此 cause 設施在 1.4 版本中首次出現。它也稱為異常鏈設施,因為 cause 自身也會有 cause,依此類推,就形成了異常鏈,每個異常都是由另一個異常引起的。
Error
Error 是 Throwable 的子類,通常情況下應用程序「不應該試圖捕獲的嚴重問題」。
Error 是程序無法處理的錯誤,表示運行應用程序中較嚴重問題。大多數錯誤與代碼編寫者執行的操作無關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。
例如:Java虛擬機運行錯誤(Virtual MachineError),當 JVM 不再有繼續執行操作所需的內存資源時,將出現 OutOfMemoryError。這些異常發生時,Java虛擬機(JVM)一般會選擇線程終止。
這些錯誤表示故障發生于虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因為它們在應用程序的控制和處理能力之 外,而且絕大多數是程序運行時不允許出現的狀況。對于設計合理的應用程序來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java中,錯誤通過Error的子類描述。
Exception
Exception以及它的子類,代表程序運行時發送的各種不期望發生的事件。可以被Java異常處理機制使用,是異常處理的核心。
Exception 異常主要分為兩類:
「1、非檢查性異常(unchecked exception)」
Error 和 RuntimeException 以及他們的子類。Java語言在編譯時,不會提示和發現這樣的異常,不要求在程序中處理這些異常。所以我們可以在程序中編寫代碼來處理(使用try…catch…finally)這樣的異常,也可以不做任何處理。對于這些錯誤或異常,我們應該修正代碼,而不是去通過異常處理器處理。這樣的異常發生的原因多半是由于我們的代碼邏輯出現了問題。
例如:
當程序中用數字除以0時,就會拋出ArithmeticException異常;
在類型轉換時,錯誤的強制類型轉換會拋出ClassCastException類型轉換異常;
當使用集合進行數組索引越界時就會拋出ArrayIndexOutOfBoundsException異常;
當程序中使用了空對象進行操作時就會拋出注明的空指針NullPointerException異常等。
「常見的非檢查性異常有」:
異常 | 描述 |
---|---|
ArithmeticException | 當出現異常的運算條件時,拋出異常。例如,一個整數“除以零”時,拋出此類的一個實例。 |
ArrayIndexOutOfBoundsException | 用非法索引訪問數組時跑出的異常。如果索引為負或大于等于數組大小,則該索引為非法索引。 |
ArrayStoreException | 試圖將錯誤類型的對象存儲到一個對象數組時,拋出的異常。 |
ClassCastException | 試圖將對象強制轉換為不是同一個類型或其子類的實例時,拋出的異常。 |
IllegalArgumentException | 當向一個方法傳遞非法或不正確的參數時,拋出該異常。 |
IllegalMonitorStateException | 當某一線程已經試圖等待對象的監視器,或者通知其他正在等待該對象監視器的線程,而該線程本身沒有獲得指定監視器時拋出該異常。 |
IllegalStateException | 在非法或不適當的時間調用方法時產生的信號。或者說Java環境或應用程序沒有處于請求操作所要求的適當狀態下。 |
IllegalThreadStateException | 線程沒有處于請求操作所要求的適當狀態時,拋出該異常。 |
IndexOutOfBoundsException | 當某種排序的索引超出范圍時拋出的異常,例如,一個數組,字符串或一個向量的排序等。 |
NegativeArraySizeException | 如果應用程序試圖創建大小為負的數組時,拋出該異常。 |
NullPointerException | 當應用程序在需要操作對象的時候而獲得的對象實例是null時拋出該異常。 |
NumberFormatException | 當應用程序試圖將字符串轉換成一種數值類型,但該字符串不能轉換為適當格式時,拋出該異常。 |
SecurityException | 由安全管理器拋出的異常,指示存在安全侵犯。 |
StringIndexOutOfBoundsException | 此異常由String方法拋出,說明索引為負或者超出了字符串的大小。 |
「2、檢查性異常(checked exception)」
除了Error 和 RuntimeException的其它異常。Java語言強制要求程序員為這樣的異常做預備處理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch語句捕獲它并處理,要么用throws子句聲明拋出它,否則編譯不會通過。這樣的異常一般是由程序的運行環境導致的。因為程序可能被運行在各種未知的環境下,而程序員無法干預用戶如何使用他編寫的程序,于是程序員就應該為這樣的異常時刻準備著。如SQLException,IOException,ClassNotFoundException 等。
檢查性異常就是指,編譯器在編譯期間要求必須得到處理的那些異常,你必須在編譯期處理了。
「常見的檢查性異常有」:
異常 | 描述 |
---|---|
ClassNotFoundException | 當應用程序試圖加載一個類,通過名字查找時卻發現沒有該類的定義時,拋出該異常。 |
CloneNotSupportedException | 當去克隆一個對象時,發現該對象沒有實現Cloneable接口時,拋出該異常。 |
IllegalAccessException | 當應用程序嘗試通過反射的方式來訪問類、成員變量或調用方法時,卻無法訪問這些類、成員變量或方法的定義時,拋出該異常。 |
InstantiationException | 當試圖使用Class類中的newInstance方法創建一個類的實例,而制定的類對象因為是一個接口或是一個抽象類而無法實例化時,拋出該異常。 |
InterruptedException | 一個線程被另一個線程中斷時,拋出該異常。 |
NoSuchFieldException | 當找不到指定的變量字段時,拋出該異常、 |
NoSuchMethodException | 當找不到指定的類方法時,拋出該異常。 |
二、初識異常
下面我們通過一個簡單實例,讓大家更直觀的認識一下Java的異常。
下面的代碼會拋出著名的空指針異常: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 "微信公眾號:我是開發者FTD"; } }
運行程序,控制臺輸出結果如下:
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」 ,然后程序就終止運行了。
三、異常處理機制
在編寫代碼處理異常時,對于檢查性異常,有兩種不同的處理方式:
使用 「try…catch…finally…」 語句塊處理
在方法中使用 「throws/throw」 關鍵詞將異常交給方法調用者去處理
try...catch...finally… 關鍵字
使用 try 和 catch 關鍵字可以捕獲異常。
try/catch 代碼塊放在異常可能發生的地方。
try/catch代碼塊中的代碼稱為保護代碼,使用 try/catch 的語法如下:
try { ... } catch (IOException ioException) { ... } catch (Exception exception) { ... } finally { ... }
「try 塊:」
try塊中放可能發生異常的代碼。
如果執行完try且不發生異常,則接著去執行finally塊中的代碼和finally后面的代碼(如果有的話)。
如果程序發生異常,則嘗試去匹配對應的catch塊。
「catch 塊:」
每一個catch塊用于捕獲并處理一個特定的異常,或者這異常類型的子類。Java7中可以將多個異常聲明在一個catch中。
catch后面的括號定義了異常類型和異常參數。如果異常與之匹配且是最先匹配到的,則虛擬機將使用這個catch塊來處理異常。
在catch塊中可以使用這個塊的異常參數來獲取異常的相關信息。異常參數是這個catch塊中的局部變量,其它塊不能訪問。
如果當前try塊中發生的異常在后續的所有catch中都沒捕獲到,則先去執行finally,然后到這個方法的外部調用者中去匹配異常處理器。
如果try中沒有發生異常,則所有的catch塊將被忽略。
「需要注意的地方」
1、try塊中的局部變量和catch塊中的局部變量(包括異常變量),以及finally中的局部變量,他們之間不可共享使用。
2、每一個catch塊用于處理一個異常。異常匹配是按照catch塊的順序從上往下尋找的,只有第一個匹配的catch會得到執行。匹配時,不僅運行精確匹配,也支持父類匹配,因此,如果同一個try塊下的多個catch異常類型有父子關系,應該將子類異常放在前面,父類異常放在后面,這樣保證每個catch塊都有存在的意義。
3、Java中,異常處理的任務就是將執行控制流從異常發生的地方轉移到能夠處理這種異常的地方去。也就是說:當一個方法的某條語句發生異常時,這條語句的后面的語句不會再執行,它失去了焦點。執行流跳轉到最近的匹配的異常處理catch代碼塊去執行,異常被處理完后,執行流會接著在“處理了這個異常的catch代碼塊”后面接著執行。
「finally 塊:」
finally塊不是必須的,通常是可選的。
無論異常是否發生,異常是否匹配被處理,finally中的代碼都會執行。
一個try至少要有一個catch塊,否則, 至少要有1個finally塊。但是finally不是用來處理異常的,finally不會捕獲和處理異常,處理異常的只能是catch塊。
finally主要做一些清理工作,如流的關閉,數據庫連接的關閉等。
finally塊不管異常是否發生,只要對應的try執行了,則它一定也執行。只有一種方法讓finally塊不執行:「System.exit()」 。
大家需要養成**良好的編程習慣是:**在try塊中打開資源,在finally塊中清理并釋放這些資源,以免造成內存泄露。
「需要注意的地方:」
1、在同一try…catch…finally…塊中,如果try中拋出異常,且有匹配的catch塊,則先執行catch塊,再執行finally塊。如果沒有catch塊匹配,則先執行finally,然后去到上層的調用者中尋找合適的catch塊。
2、在同一try…catch…finally…塊中 ,try發生異常,且匹配的catch塊中處理異常時也拋出異常,那么后面的finally也會執行:首先執行finally塊,然后去上層調用者中尋找合適的catch塊。
throws/throw 關鍵字
「throws 關鍵字」
如果一個方法內部的代碼會拋出檢查性異常(checked exception),而方法自己又沒有對這些異常完全處理掉,則java的編譯器會要求你必須在方法的簽名上使用 「throws」 關鍵字聲明這些可能拋出的異常,否則編譯不通過。
throws 是另一種處理異常的方式,它不同于try…catch…finally…,throws 關鍵字僅僅是將方法中可能出現的異常向調用者拋出,而自己則不具體處理。
采取這種異常處理的原因可能是:方法本身不知道如何處理這樣的異常,或者說讓調用者處理更好,調用者需要為可能發生的異常負責。
「throw 關鍵字」
我們也可以通過 throw 語句手動顯式的拋出一個異常,throw語句的后面必須是一個異常對象。語法如下:
throw exceptionObject
throw 語句必須寫在方法中,執行throw 語句的地方就是一個異常拋出點,它和由JRE自動形成的異常拋出點沒有任何差別。
public void save(User user) { if (user == null) throw new IllegalArgumentException("User對象為空"); //...... }
try-catch-finally 的執行順序
try-catch-finally 執行順序的相關問題可以說是各種面試中的「常客」了,尤其是 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; }
大家不妨算一算程序員最終運行的結果是什么。
輸出結果如下:
try block, i = 2 finally block i = 10 10
這算一個相當簡單的問題了,沒有坑,下面我們稍微改動一下:
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; }
輸出結果如下:
catch block i = 1 finally block i = 10 10
運行結果想必也是意料之中吧,程序拋出一個異常,然后被本方法的 catch 塊捕獲并進行了處理。
「面試題二:」
public static void main(String[] args) { int result = test3(); System.out.println(result); } public static int test3() { //try 語句塊中有 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 = 10; System.out.println("finally block i = " + i); } }
輸出結果如下:
try block, i = 2 finally block i = 10 2
是不是有點疑惑?明明我 try 語句塊中有 return 語句,可為什么最終還是執行了 finally 塊中的代碼?
我們反編譯這個類,看看這個 test3 方法編譯后的字節碼的實現:
0: iconst_1 //將 1 加載進操作數棧 1: istore_0 //將操作數棧 0 位置的元素存進局部變量表 2: iinc 0, 1 //將局部變量表 0 位置的元素直接加一(i=2) 5: getstatic #3 // 5-27 行執行的 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 位置的元素加載進操作棧(2) 31: istore_1 //把操作棧頂的元素存入局部變量表位置 1 處 32: bipush 10 //加載一個常量到操作棧(10) 34: istore_0 //將 10 存入局部變量表 0 處 35: getstatic #3 //35-57 行執行 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 位置的元素加載進操作棧(2) 61: ireturn //將操作棧頂元素返回(2) -------------------try + finally 結束 ------------ ------------------下面是 catch + finally,類似的 ------------ 62: astore_1 63: iinc 0, 1 ....... .......
從我們的分析中可以看出來,finally 代碼塊中的內容始終會被執行,無論程序是否出現異常的原因就是,「編譯器會將 finally 塊中的代碼復制兩份并分別添加在 try 和 catch 的后面」。
可能有人會所疑惑,原本我們的 i 就被存儲在局部變量表 0 位置,而最后 finally 中的代碼也的確將 slot 0 位置填充了數值 10,可為什么最后程序依然返回的數值 2 呢?
仔細看字節碼,你會發現在 return 語句返回之前,虛擬機會將待返回的值壓入操作數棧,等待返回,即使 finally 語句塊對 i 進行了修改,但是待返回的值已經確實的存在于操作數棧中了,所以不會影響程序返回結果。
「面試題三:」
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; } }
運行結果:
try block, i = 2 finally block i = 3 3
其實你從它的字節碼指令去看整個過程,而不要單單死記它的執行過程。
你會發現程序最終會采用 finally 代碼塊中的 return 語句進行返回,而直接忽略 try 語句塊中的 return 指令。
自定義異常
Java 的異常機制中所定義的所有異常不可能預見所有可能出現的錯誤,某些特定的情境下,則需要我們自定義異常類型來向上報告某些錯誤信息。
而自定義異常類型也是相當簡單的,你可以選擇繼承 Throwable,Exception 或它們的子類,甚至你不需要實現和重寫父類的任何方法即可完成一個異常類型的定義。
例如:
public class MyException extends RuntimeException{ } public class MyException extends Exception{ }
按照國際慣例,自定義的異常應該總是包含如下的構造函數:
一個無參構造函數
一個帶有String參數的構造函數,并傳遞給父類的構造函數。
一個帶有String參數和Throwable參數,并都傳遞給父類構造函數
一個帶有Throwable 參數的構造函數,并傳遞給父類的構造函數。
下面是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); } }
異常的注意事項
1、當子類重寫父類的帶有 throws聲明的函數時,其throws聲明的異常必須在父類異常的可控范圍內——用于處理父類的throws方法的異常處理器,必須也適用于子類的這個帶throws方法 。這是為了支持多態。
例如,父類方法throws 的是2個異常,子類就不能throws 3個及以上的異常。父類throws IOException,子類就必須throws IOException或者IOException的子類。
2、Java程序可以是多線程的。每一個線程都是一個獨立的執行流,獨立的函數調用棧。如果程序只有一個線程,那么沒有被任何代碼處理的異常 會導致程序終止。如果是多線程的,那么沒有被任何代碼處理的異常僅僅會導致異常所在的線程結束。
也就是說,Java中的異常是線程獨立的,線程的問題應該由線程自己來解決,而不要委托到外部,也不會直接影響到其它線程的執行。
異常使用時的常見錯誤
1、將異常直接顯示在頁面或客戶端
將異常直接打印在客戶端的例子屢見不鮮,一旦程序運行出現異常,默認情況下容器將異常堆棧信息直接打印在頁面上。從客戶角度來說,任何異常都沒有實際意義,絕大多數的客戶也根本看不懂異常信息,軟件開發也要盡量避免將異常直接呈現給用戶,一定要在前端展示層對異常進行封裝后展示。目前絕大多數應用都是前后端分離的模式,這種直接打印異常的情況已經相對改善了很多,不過我們在編碼時還是要特別注意下這個原則。
2、忽略異常
如下異常處理只是將異常輸出到控制臺,沒有任何意義。而且這里出現了異常并沒有中斷程序,進而調用代碼繼續執行,導致更多的異常。
public void retrieveObjectById(Long id) { try { //..some code that throws SQLException } catch (SQLException ex) { /** *了解的人都知道,這里的異常打印毫無意義,僅僅是將錯誤堆棧輸出到控制臺。 * 而在 Production 環境中,需要將錯誤堆棧輸出到日志。 * 而且這里 catch 處理之后程序繼續執行,會導致進一步的問題*/ ex.printStacktrace(); } }
捕獲了異常缺不進行處理,這是我們在寫代碼時候的大忌,可以重構成:
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、將異常包含在循環語句塊中
如下代碼所示,異常包含在 for 循環語句塊中。
for (int i = 0; i < 100; i++) { try { } catch (XXXException e) { //.... } }
我們都知道異常處理占用系統資源。一看,大家都認為不會犯這樣的錯誤。換個角度,類 A 中執行了一段循環,循環中調用了 B 類的方法,B 類中被調用的方法卻又包含 try-catch 這樣的語句塊。褪去類的層次結構,代碼和上面如出一轍。
4、利用 Exception 捕捉所有潛在的異常
一段方法執行過程中拋出了幾個不同類型的異常,為了代碼簡潔,利用基類 Exception 捕捉所有潛在的異常,如下例所示:
public void retrieveObjectById(Long id) { try { //...拋出 IOException 的代碼調用 //...拋出 SQLException 的代碼調用 } catch (Exception e) { //這里利用基類 Exception 捕捉的所有潛在的異常,如果多個層次這樣捕捉,會丟失原始異常的有效信息 throw new RuntimeException("Exception in retieveObjectById”, e); } }
估計大部分程序員都會有這種寫法,為了省事簡便,直接一個頂層的exception來捕獲所有可能出現的異常,這樣雖然可以保證異常肯定會被捕捉到,但是程序卻無法針對不同的錯誤異常進行對應正確的處理,可以重構成:
public void retrieveObjectById(Long id) { try { //..some code that throws RuntimeException, IOException, SQLException } catch (IOException e) { //僅僅捕捉 IOException throw new RuntimeException(/*指定這里 IOException 對應的錯誤代碼*/code, "Exception in retieveObjectById”, e); } catch (SQLException e) { //僅僅捕捉 SQLException throw new RuntimeException(/*指定這里 SQLException 對應的錯誤代碼*/code, "Exception in retieveObjectById”, e); } }
5、異常包含的信息不能充分定位問題
異常不僅要能夠讓開發人員知道哪里出了問題,更多時候開發人員還需要知道是什么原因導致的問題,我們知道 java .lang.Exception 有字符串類型參數的構造方法,這個字符串可以自定義成通俗易懂的提示信息。
簡單的自定義信息開發人員只能知道哪里出現了異常,但是很多的情況下,開發人員更需要知道是什么參數導致了這樣的異常。這個時候我們就需要將方法調用的參數信息追加到自定義信息中。下例只列舉了一個參數的情況,多個參數的情況下,可以單獨寫一個工具類組織這樣的字符串。
public void retieveObjectById(Long id) { try { //..some code that throws SQLException } catch (SQLException ex) { //將參數信息添加到異常信息中 throw new RuntimeException("Exception in retieveObjectById with Object Id :"+ id, ex); } }
到此,關于“Java中重要的錯誤處理機制異常有哪些”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。