一、異常概述 異常是程式中的一些錯誤,但並不是所有的錯誤都是異常,並且錯誤有時候是可以避免的。比如說,你的代碼少了一個分號,那麼運行出來結果是提示是錯誤 java.lang.Error;如果你用System.out.println(11/0),那麼你是因為你用0做了除數,會拋出 java.lang. ...
一、異常概述
異常是程式中的一些錯誤,但並不是所有的錯誤都是異常,並且錯誤有時候是可以避免的。比如說,你的代碼少了一個分號,那麼運行出來結果是提示是錯誤 java.lang.Error;如果你用System.out.println(11/0),那麼你是因為你用0做了除數,會拋出 java.lang.ArithmeticException 的異常。
異常發生的原因有很多,通常包含以下幾大類:
- 用戶輸入了非法數據。
- 要打開的文件不存在。
- 網路通信時連接中斷,或者JVM記憶體溢出。
這些異常有的是因為用戶錯誤引起,有的是程式錯誤引起的,還有其它一些是因為物理錯誤引起的,為增強程式的健壯性,電腦程式的編寫也需要考慮處理這些異常情況,Java語言提供了異常處理功能,本文將介紹Java異常處理機制。
為了更好的理解和學習Java異常處理機制,首先看看下麵程式:
//HelloWorld.java文件 package com.Kevin; public class HelloWorld { public static void main(String[] args) { int a = 0; System.out.println(5 / a); } }
這個程式沒有編譯錯誤,但會發生如下的運行時錯誤:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.Kevin.HelloWorld.main(HelloWorld.java:9)
在數學上除數不能為0,所以程式運行時表達式(5 / a)會拋出ArithmeticException異常,ArithmeticException是數學計算異常,凡是發生數學計算錯誤都會拋出該異常。
程式運行過程中難免會發生異常,發生異常並不可怕,程式員應該考慮到有可能發生這些異常,編程時應該捕獲併進行處理異常,不能讓程式發生終止,這就是健壯的程式。
二、異常類繼承層次
所有的異常類是從 java.lang.Exception 類繼承的子類。Exception 類是 Throwable 類的子類。除了Exception類外,Throwable還有一個子類Error 。Java 程式通常不捕獲錯誤。錯誤一般發生在嚴重故障時,它們在Java程式處理的範疇之外。Error 用來指示運行時環境發生的錯誤。例如,JVM 記憶體溢出。一般地,程式不會從錯誤中恢復。異常類有兩個主要的子類:IOException 類和 RuntimeException 類。Java的異常類繼承層次具體如下圖:
Java異常類繼承層次圖
2.1 Throwable類
從Java異常類繼承層次圖可見,所有的異常類都直接或間接地繼承於java.lang.Throwable類,在Throwable類有幾個非常重要的方法:
- String getMessage():獲得發生異常的詳細消息。
- void printStackTrace():列印異常堆棧跟蹤信息。
- String toString():獲得異常對象的描述。
為了介紹Throwable類的使用,示例代碼如下:
1 //HelloWorld.java文件 2 package com.Kevin; 3 4 public class HelloWorld { 5 6 public static void main(String[] args) { 7 8 int a = 0; 9 int result = divide(5, a); 10 System.out.printf("divide(%d, %d) = %d", 5, a, result); 11 } 12 13 public static int divide(int number, int divisor) { 14 15 try { 16 return number / divisor; 17 } catch (Throwable throwable) { 18 19 System.out.println("getMessage() : " + throwable.getMessage()); 20 21 System.out.println("toString() : " + throwable.toString()); 22 23 System.out.println("printStackTrace()輸出信息如下:"); 24 throwable.printStackTrace(); 25 } 26 27 return 0; 28 } 29 }
運行結果如下:
getMessage() : / by zero toString() : java.lang.ArithmeticException: / by zero printStackTrace()輸出信息如下: java.lang.ArithmeticException: / by zero at com.Kevin.HelloWorld.divide(HelloWorld.java:17) at com.Kevin.HelloWorld.main(HelloWorld.java:10) divide(5, 0) = 0
將可以發生異常的語句System.out.println(5 / a)放到try-catch代碼塊中,稱為捕獲異常,有關捕獲異常的相關知識會在下部分詳細介紹。在catch中有一個Throwable對象throwable,throwable對象是系統在程式發生異常時創建,通過throwable對象可以調用Throwable中定義的方法。
代碼第19行是調用getMessage()方法獲得異常消息,輸出結果是“/ by zero”。代碼第21行是調用toString()方法獲得異常對象的描述,輸出結果是java.lang.ArithmeticException: / by zero。代碼第24行是調用printStackTrace()方法列印異常堆棧跟蹤信息。
Tips: 堆棧跟蹤信息從下往上,是方法調用的順序。首先JVM調用是 com.Kevin.HelloWorld 類的main方法,接著在HelloWorld.java源代碼第10行調用 com.Kevin.HelloWorld 類的 divide 方法,在HelloWorld.java 源代碼第17行發生了異常,最後輸出的是異常信息。
2.2 Error 和 Exception
從Java異常類繼承層次圖可見,Throwable有兩個直接子類:Error和Exception。
-
Error:Error 是程式無法恢復的嚴重錯誤,程式員根本無能為力,只能讓程式終止。例如:JVM內部錯誤、記憶體溢出和資源耗盡等嚴重情況。
-
Exception:Exception 是程式可以恢復的異常,它是程式員所能掌控的。例如:除零異常、空指針訪問、網路連接中斷和讀取不存在的文件等。本章所討論的異常處理就是對Exception及其子類的異常處理。
2.3 受檢查異常和運行時異常
從Java異常類層次圖可見,Exception類可以分為:受檢查異常和運行時異常。
-
受檢查異常
如Java異常類層次圖所示,受檢查異常是除 RuntimeException 以外的異常類。它們的共同特點是:編譯器會檢查這類異常是否進行了處理,即要麼捕獲(try-catch語句),要麼不拋出(通過在方法後聲明throws),否則會發生編譯錯誤。它們種類很多,前面遇到過的日期解析異常 ParseException。
-
運行時異常
運行時異常是繼承 RuntimeException 類的直接或間接子類。運行時異常往往是程式員所犯錯誤導致的,健壯的程式不應該發生運行時異常。它們的共同特點是:編譯器不檢查這類異常是否進行了處理,也就是對於這類異常不捕獲也不拋出,程式也可以編譯通過。由於沒有進行異常處理,一旦運行時異常發生就會導致程式的終止,這是用戶不希望看到的。由於2.1部分除零示例的ArithmeticException異常屬於RuntimeException異常,如Java異常類層次圖所示,可以不用加try-catch語句捕獲異常。
//HelloWorld.java文件 package com.Kevin; public class HelloWorld { public static void main(String[] args) { int a = 0; int result = divide(5, a); System.out.printf("divide(%d, %d) = %d", 5, a, result); } public static int divide(int number, int divisor) { //判斷除數divisor非零,防止運行時異常 if (divisor != 0) { return number / divisor; } return 0; } }
Tips: 對於運行時異常通常不採用拋出或捕獲處理方式,而是應該提前預判,防止這種發生異常,做到未雨綢繆。例如2.1部分除零示例,在進行除法運算之前應該判斷除數是非零的,修改示例代碼如下,從代碼可見提前預判這樣處理要比通過try-catch捕獲異常要友好的多。
2.4 Java常用內置異常
Java 語言定義了一些異常類在 java.lang 標準包中。標準運行時異常類的子類是最常見的異常類。由於 java.lang 包是預設載入到所有的 Java 程式的,所以大部分從運行時異常類繼承而來的異常都可以直接使用。Java 根據各個類庫也定義了一些其他的異常,下麵的表中列出了 Java 的非檢查性異常。
下麵的表中列出了 Java 定義在 java.lang 包中的檢查性異常類。
Tips:
- 檢查性異常: 不處理編譯不能通過
- 非檢查性異常:不處理編譯可以通過,如果有拋出直接拋到控制台
- 運行時異常: 就是非檢查性異常
- 非運行時異常: 就是檢查性異常
5.常見異常方法
下麵的列表是 Throwable 類的主要方法:
6.通用異常
在Java中定義了兩種類型的異常和錯誤。
- JVM(Java虛擬機) 異常:由 JVM 拋出的異常或錯誤。例如:NullPointerException 類,ArrayIndexOutOfBoundsException 類,ClassCastException 類。
- 程式級異常:由程式或者API程式拋出的異常。例如 IllegalArgumentException 類,IllegalStateException 類。
三、捕獲異常
在學習本內容之前,你先考慮一下,在現實生活中是如何對待領導交給你的任務呢?當然無非是兩種:自己有能解決的自己處理;自己無力解決的反饋給領導,讓領導自己處理。
那麼對待受檢查異常亦是如此。當前方法有能力解決,則捕獲異常進行處理;沒有能力解決,則拋出給上層調用方法處理。如果上層調用方法還無力解決,則繼續拋給它的上層調用方法,異常就是這樣向上傳遞直到有方法處理它,如果所有的方法都沒有處理該異常,那麼JVM會終止程式運行。
3.1 try-catch 語句
捕獲異常是通過try-catch語句實現的,最基本try-catch語句語法如下:
try{ //可能會發生異常的語句 } catch(Throwable e){ //處理異常e }
每個try代碼塊可以伴隨一個或多個catch代碼塊,用於處理try代碼塊中所可能發生的多種異常。catch(Throwable e)語句中的e是捕獲異常對象,e必須是Throwable的子類,異常對象e的作用域在該catch代碼塊中。下麵看看一個try-catch示例:
1 //HelloWorld.java文件 2 package com.Kevin; 3 4 import java.text.DateFormat; 5 import java.text.ParseException; 6 import java.text.SimpleDateFormat; 7 import java.util.Date; 8 9 public class HelloWorld { 10 11 public static void main(String[] args) { 12 Date date = readDate(); 13 System.out.println("日期 = " + date); 14 } 15 16 // 解析日期 17 public static Date readDate() { 18 19 try { 20 String str = "2018-4-28"; //"201A-18-18" 21 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 22 // 從字元串中解析日期 23 Date date = df.parse(str); 24 return date; 25 } catch (ParseException e) { 26 System.out.println("處理ParseException…"); 27 e.printStackTrace(); 28 } 29 return null; 30 } 31 }
上述代碼第17行定義了一個靜態方法用來將字元串解析成日期,但並非所有的字元串都是有效的日期字元串,因此調用代碼第23行的解析方法parse()有可能發生ParseException異常,ParseException是受檢查異常,在本例中使用try-catch捕獲。代碼第25行的e就是ParseException對象。代碼第27行e.printStackTrace()是列印異常堆棧跟蹤信息,本例中的"2018-4-28"字元串是有個有效的日期字元串,因此不會發生異常。如果將字元串改為無效的日期字元串,如"201A-18-18",則會列印信息。
處理ParseException java.text.ParseException: Unparseable date: "201A-18-18" 日期 = null at java.text.DateFormat.parse(Unknown Source) at com.Kevin.HelloWorld.readDate(HelloWorld.java:24) at com.Kevin.HelloWorld.main(HelloWorld.java:13)
Tips: 靜態方法、實例方法和構造方法都可以聲明拋出異常,凡是拋出異常的方法都可以通過try-catch進行捕獲,當然運行時異常可以不捕獲。一個方法聲明拋出什麼樣的異常需要查詢API文
3.2 多catch代碼塊
如果try代碼塊中有很多語句會發生異常,而且發生的異常種類又很多。那麼可以在try後面跟有多個catch代碼塊。多catch代碼塊語法如下:
try{ //可能會發生異常的語句 } catch(Throwable e){ //處理異常e } catch(Throwable e){ //處理異常e } catch(Throwable e){ //處理異常e }
在多個catch代碼情況下,當一個catch代碼塊捕獲到一個異常時,其他的catch代碼塊就不再進行匹配。
Tips: 當捕獲的多個異常類之間存在父子關係時,捕獲異常順序與catch代碼塊的順序有關。一般先捕獲子類,後捕獲父類,否則子類捕獲不到。
示例代碼如下:
1 //HelloWorld.java文件 2 package com.Kevin; 3 4 …… 5 6 public class HelloWorld { 7 8 public static void main(String[] args) { 9 Date date = readDate(); 10 System.out.println("讀取的日期 = " + date); 11 } 12 13 public static Date readDate() { 14 15 FileInputStream readfile = null; 16 InputStreamReader ir = null; 17 BufferedReader in = null; 18 try { 19 readfile = new FileInputStream("readme.txt"); 20 ir = new InputStreamReader(readfile); 21 in = new BufferedReader(ir); 22 // 讀取文件中的一行數據 23 String str = in.readLine(); 24 if (str == null) { 25 return null; 26 } 27 28 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 29 Date date = df.parse(str); 30 return date; 31 32 } catch (FileNotFoundException e) { 33 System.out.println("處理FileNotFoundException..."); 34 e.printStackTrace(); 35 } catch (IOException e) { 36 System.out.println("處理IOException..."); 37 e.printStackTrace(); 38 } catch (ParseException e) { 39 System.out.println("處理ParseException..."); 40 e.printStackTrace(); 41 } 42 return null; 43 } 44 45 }
上述代碼通過Java I/O(輸入輸出)流技術從文件readme.txt中讀取字元串,然後解析成為日期。由於Java I/O技術還沒有介紹,讀者先不要關註I/O技術細節,這考慮調用它們的方法會發生異常就可以了。
在try代碼塊中第19行代碼調用FileInputStream構造方法可以會發生FileNotFoundException異常。第23行代碼調用BufferedReader輸入流的readLine()方法可以會發生IOException異常。從Java異常類繼承層次圖可見FileNotFoundException異常是IOException異常的子類,應該先FileNotFoundException捕獲,見代碼第32行;後捕獲IOException,見代碼第35行。
如果將FileNotFoundException和IOException捕獲順序調換,代碼如下:
try{ //可能會發生異常的語句 } catch (IOException e) { // IOException異常處理 } catch (FileNotFoundException e) { // FileNotFoundException異常處理 }
那麼第二個catch代碼塊永遠不會進入,FileNotFoundException異常處理永遠不會執行。
由於上述代碼第38行ParseException異常與IOException和FileNotFoundException異常沒有父子關係,捕獲ParseException異常位置可以隨意放置。
3.3 try-catch 語句嵌套
Java提供的try-catch語句嵌套是可以任意嵌套,修改3.2部分示例代碼如下:
1 //HelloWorld.java文件 2 package com.Kevin; 3 … … 4 public class HelloWorld { 5 6 public static void main(String[] args) { 7 Date date = readDate(); 8 System.out.println("讀取的日期 = " + date); 9 } 10 11 public static Date readDate() { 12 13 FileInputStream readfile = null; 14 InputStreamReader ir = null; 15 BufferedReader in = null; 16 try { 17 readfile = new FileInputStream("readme.txt"); 18 ir = new InputStreamReader(readfile); 19 in = new BufferedReader(ir); 20 21 try { 22 String str = in.readLine(); 23 if (str == null) { 24 return null; 25 } 26 27 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 28 Date date = df.parse(str); 29 return date; 30 31 } catch (ParseException e) { 32 System.out.println("處理ParseException..."); 33 e.printStackTrace(); 34 } 35 36 } catch (FileNotFoundException e) { 37 System.out.println("處理FileNotFoundException..."); 38 e.printStackTrace(); 39 } catch (IOException e) { 40 System.out.println("處理IOException..."); 41 e.printStackTrace(); 42 } 43 return null; 44 } 45 }
上述代碼第21~34行是捕獲ParseException異常try-catch語句,可見這個try-catch語句就是嵌套在捕獲IOException和FileNotFoundException異常的try-catch語句中。
程式執行時內層如果會發生異常,首先由內層catch進行捕獲,如果捕獲不到,則由外層catch捕獲。例如:代碼第22行的readLine()方法可能發生IOException異常,該異常無法被內層catch捕獲,最後被代碼第39行的外層catch捕獲。
Tips: try-catch不僅可以嵌套在try代碼塊中,還可以嵌套在catch代碼塊或finally代碼塊,finally代碼塊後面會詳細介紹。try-catch嵌套會使程式流程變的複雜,如果能用多catch捕獲的異常,儘量不要使用try-catch嵌套。特別對於初學者不要簡單地使用Eclipse的語法提示不加區分地添加try-catch嵌套,要梳理好程式的流程再考慮try-catch嵌套的必要性。
3.4 多重捕捉
多catch代碼塊客觀上提高了程式的健壯性,但是程式代碼量大大增加。如果有些異常雖然種類不同,但捕獲之後的處理是相同的,看如下代碼。
try{ //可能會發生異常的語句 } catch (FileNotFoundException e) { //調用方法methodA處理 } catch (IOException e) { //調用方法methodA處理 } catch (ParseException e) { //調用方法methodA處理 }
三個不同類型的異常,要求捕獲之後的處理都是調用methodA方法。是否可以把這些異常合併處理,Java 7推出了多重捕獲(multi-catch)技術,可以幫助解決此類問題,上述代碼修改如下:
try{ //可能會發生異常的語句 } catch (IOException | ParseException e) { //調用方法methodA處理 }
在catch中多重捕獲異常用“|”運算符連接起來。
Tips: 有的讀者會問什麼不寫成FileNotFoundException | IOException | ParseException 呢?這是因為由於FileNotFoundException屬於IOException異常,IOException異常可以捕獲它的所有子類異常了。
四、釋放資源
有時在try-catch語句中會占用一些非Java資源,如:打開文件、網路連接、打開資料庫連接和使用數據結果集等,這些資源並非Java資源,不能通過JVM的垃圾收集器回收,需要程式員釋放。為了確保這些資源能夠被釋放可以使用finally代碼塊或Java 7之後提供自動資源管理(Automatic Resource Management)技術。
4.1 finally 代碼塊
try-catch語句後面還可以跟有一個finally代碼塊,try-catch-finally語句語法如下:
try{ //可能會生成異常語句 } catch(Throwable e1){ //處理異常e1 } catch(Throwable e2){ //處理異常e2 } catch(Throwable eN){ //處理異常eN } finally{ //釋放資源 }
無論try正常結束還是catch異常結束都會執行finally代碼塊,如下圖所示:
使用finally代碼塊示例代碼如下:
1 //HelloWorld.java文件 2 package com.Kevin; 3 4 … … 5 6 public class HelloWorld { 7 8 public static void main(String[] args) { 9 Date date = readDate(); 10 System.out.println("讀取的日期 = " + date); 11 } 12 13 public static Date readDate() { 14 15 FileInputStream readfile = null; 16 InputStreamReader ir = null; 17 BufferedReader in = null; 18 try { 19 readfile = new FileInputStream("readme.txt"); 20 ir = new InputStreamReader(readfile); 21 in = new BufferedReader(ir); 22 // 讀取文件中的一行數據 23 String str = in.readLine(); 24 if (str == null) { 25 return null; 26 } 27 28 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 29 Date date = df.parse(str); 30 return date; 31 32 } catch (FileNotFoundException e) { 33 System.out.println("處理FileNotFoundException..."); 34 e.printStackTrace(); 35 } catch (IOException e) { 36 System.out.println("處理IOException..."); 37 e.printStackTrace(); 38 } catch (ParseException e) { 39 System.out.println("處理ParseException..."); 40 e.printStackTrace(); 41 } finally { 42 try { 43 if (readfile != null) { 44 readfile.close(); 45 } 46 } catch (IOException e) { 47 e.printStackTrace(); 48 } 49 try { 50 if (ir != null) { 51 ir.close(); 52 } 53 } catch (IOException e) { 54 e.printStackTrace(); 55 } 56 try { 57 if (in != null) { 58 in.close(); 59 } 60 } catch (IOException e) { 61 e.printStackTrace(); 62 } 63 } 64 65 return null; 66 } 67 }
上述代碼第41行~第63行是finally語句,在這裡通過關閉流釋放資源,FileInputStream、InputStreamReader和BufferedReader是三個輸入流,它們都需要關閉,見代碼第44行~第58行通過流的close()關閉流,但是流的close()方法還有可以能發生 IOException 異常,所以這裡又針對每一個close()語句還需要進行捕獲處理。
Tips: 為了代碼簡潔等目的,可能有的人會將finally代碼中的多個嵌套的try-catch語句合併,例如將上述代碼改成如下形式,將三個有可以發生異常的close()方法放到一個try-catch。讀者自己考慮一下這處理是否穩妥呢?每一個close()方法對應關閉一個資源,如果第一個close()方法關閉時發生了異常,那麼後面的兩個也不會關閉,因此如下的程式代碼是有缺陷的。
try { ... ... } catch (FileNotFoundException e) { ... ... } catch (IOException e) { ... ... } catch (ParseException e) { ... ... } finally { try { if (readfile != null) { readfile.close(); } if (ir != null) { ir.close(); } if (in != null) { in.close(); } } catch (IOException e) { e.printStackTrace(); } }
4.2 自動資源管理
4.1部分使用finally代碼塊釋放資源會導致程式代碼大量增加,一個finally代碼塊往往比正常執行的程式還要多。在Java 7之後提供自動資源管理(Automatic Resource Management)技術,可以替代finally代碼塊,優化代碼結構,提高程式可讀性。自動資源管理是在try語句上的擴展,語法如下:
try (聲明或初始化資源語句) { //可能會生成異常語句 } catch(Throwable e1){ //處理異常e1 } catch(Throwable e2){ //處理異常e1 } catch(Throwable eN){ //處理異常eN }
在try語句後面添加一對小括弧“()”,其中是聲明或初始化資源語句,可以有多條語句語句之間用分號“;”分隔。
示例代碼如下:
1 //HelloWorld.java文件 2 package com.Kevin; 3 … … 4 public class HelloWorld { 5 6 public static void main(String[] args) { 7 Date date = readDate(); 8 System.out.println("讀取的日期 = " + date); 9 } 10 11 public static Date readDate() { 12 13 // 自動資源管理 14 try (FileInputStream readfile = new FileInputStream("readme.txt"); 15 InputStreamReader ir = new InputStreamReader(readfile); 16 BufferedReader in = new BufferedReader(ir)) { 17 18 // 讀取文件中的一行數據 19 String str = in.readLine(); 20 if (str == null) { 21 return null; 22 } 23 24 DateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 25 Date date = df.parse(str); 26 return date; 27 28 } catch (FileNotFoundException e) { 29 System.out.println("處理FileNotFoundException..."); 30 e.printStackTrace(); 31 } catch (IOException e) { 32 System.out.println("處理IOException..."); <