《Java從小白到大牛》之第14章 異常處理(上)
《Java從小白到大牛》紙質版已經上架了!!!
很多事件並非總是按照人們自己設計意願順利發展的,而是有能夠出現這樣那樣的異常情況。例如:你計劃週末郊遊,你的計劃會安排滿滿的,你計劃可能是這樣的:從家裡出發→到達目的→游泳→燒烤→回家。但天有不測風雲,當前你準備燒烤時候天降大雨,你只能終止郊遊提前回家。“天降大雨”是一種異常情況,你的計劃應該考慮到這樣情況,並且應該有處理這種異常的預案。
為增強程式的健壯性,計算機程式的編寫也需要考慮處理這些異常情況,Java語言提供了異常處理功能,本章介紹Java異常處理機制。
從一個問題開始
為了學習Java異常處理機制,首先看看下面程式。
//HelloWorld.java檔案
package com.a51work6;
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.a51work6.HelloWorld.main(HelloWorld.java:9)
在數學上除數不能為0,所以程式執行時表示式(5 / a)會丟擲ArithmeticException異常,ArithmeticException是數學計算異常,凡是發生數學計算錯誤都會丟擲該異常。
程式執行過程中難免會發生異常,發生異常並不可怕,程式設計師應該考慮到有可能發生這些異常,程式設計時應該捕獲並進行處理異常,不能讓程式發生終止,這就是健壯的程式。
異常類繼承層次
異常封裝成為類Exception,此外,還有Throwable和Error類,異常類繼承層次如圖14-1所示。
Throwable類 {#throwable}
從圖14-1可見,所有的異常類都直接或間接地繼承於java.lang.Throwable類,在Throwable類有幾個非常重要的方法:
- String getMessage():獲得發生異常的詳細訊息。
- void printStackTrace():列印異常堆疊跟蹤資訊。
- String toString():獲得異常物件的描述。
提示 堆疊跟蹤是方法呼叫過程的軌跡,它包含了程式執行過程中方法呼叫的順序和所在原始碼行號。
為了介紹Throwable類的使用,下面修改14.1節的示例程式碼如下:
//HelloWorld.java檔案
package com.a51work6;
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) {
try {
return number / divisor;
} catch (Throwable throwable) { ①
System.out.println("getMessage() : " + throwable.getMessage()); ②
System.out.println("toString() : " + throwable.toString()); ③
System.out.println("printStackTrace()輸出資訊如下:");
throwable.printStackTrace(); ④
}
return 0;
}
}
執行結果如下:
getMessage() : / by zero
toString() : java.lang.ArithmeticException: / by zero
printStackTrace()輸出資訊如下:
java.lang.ArithmeticException: / by zero
at com.a51work6.HelloWorld.divide(HelloWorld.java:17)
at com.a51work6.HelloWorld.main(HelloWorld.java:10)
divide(5, 0) = 0
將可以發生異常的語句System.out.println(5 / a)放到try-catch程式碼塊中,稱為捕獲異常,有關捕獲異常的相關知識會在下一節詳細介紹。在catch中有一個Throwable物件throwable,throwable物件是系統在程式發生異常時建立,通過throwable物件可以呼叫Throwable中定義的方法。
程式碼第②行是呼叫getMessage()方法獲得異常訊息,輸出結果是“/ by zero”。程式碼第③行是呼叫toString()方法獲得異常物件的描述,輸出結果是java.lang.ArithmeticException: / by zero。程式碼第④行是呼叫printStackTrace()方法列印異常堆疊跟蹤資訊。
提示 堆疊跟蹤資訊從下往上,是方法呼叫的順序。首先JVM呼叫是com.a51work6.HelloWorld類的main方法,接著在HelloWorld.java原始碼第10行呼叫com.a51work6.HelloWorld類的divide方法,在HelloWorld.java原始碼第17行發生了異常,最後輸出的是異常資訊。
Error和Exception {#error-exception}
從圖14-1可見,Throwable有兩個直接子類:Error和Exception。
- Error
Error是程式無法恢復的嚴重錯誤,程式設計師根本無能為力,只能讓程式終止。例如:JVM內部錯誤、記憶體溢位和資源耗盡等嚴重情況。
- Exception
Exception是程式可以恢復的異常,它是程式設計師所能掌控的。例如:除零異常、空指標訪問、網路連線中斷和讀取不存在的檔案等。本章所討論的異常處理就是對Exception及其子類的異常處理。
受檢查異常和執行時異常 {#-0}
從圖14-1可見,Exception類可以分為:受檢查異常和執行時異常。
- 受檢查異常
如圖14-1所示,受檢查異常是除RuntimeException以外的異常類。它們的共同特點是:編譯器會檢查這類異常是否進行了處理,即要麼捕獲(try-catch語句),要麼不丟擲(通過在方法後宣告throws),否則會發生編譯錯誤。它們種類很多,前面遇到過的日期解析異常ParseException。
- 執行時異常
執行時異常是繼承RuntimeException類的直接或間接子類。執行時異常往往是程式設計師所犯錯誤導致的,健壯的程式不應該發生執行時異常。它們的共同特點是:編譯器不檢查這類異常是否進行了處理,也就是對於這類異常不捕獲也不丟擲,程式也可以編譯通過。由於沒有進行異常處理,一旦執行時異常發生就會導致程式的終止,這是使用者不希望看到的。由於14.2.1節除零示例的ArithmeticException異常屬於RuntimeException異常,見圖14-1所示,可以不用加try-catch語句捕獲異常。
提示 對於執行時異常通常不採用丟擲或捕獲處理方式,而是應該提前預判,防止這種發生異常,做到未雨綢繆。例如14.2.1節除零示例,在進行除法運算之前應該判斷除數是非零的,修改示例程式碼如下,從程式碼可見提前預判這樣處理要比通過try-catch捕獲異常要友好的多。
//HelloWorld.java檔案
package com.a51work6;
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;
}
}
除了圖14-1所示異常,還有很多異常,本書不能一一窮盡,隨著學習的深入會介紹一些常用的異常,其他異常讀者可以自己查詢API文件。
捕獲異常
在學習本內容之前,你先考慮一下,在現實生活中是如何對待領導交給你的任務呢?當然無非是兩種:自己有能解決的自己處理;自己無力解決的反饋給領導,讓領導自己處理。
那麼對待受檢查異常亦是如此。當前方法有能力解決,則捕獲異常進行處理;沒有能力解決,則丟擲給上層呼叫方法處理。如果上層呼叫方法還無力解決,則繼續拋給它的上層呼叫方法,異常就是這樣向上傳遞直到有方法處理它,如果所有的方法都沒有處理該異常,那麼JVM會終止程式執行。
這一節先介紹一下捕獲異常。
try-catch語句 {#try-catch}
捕獲異常是通過try-catch語句實現的,最基本try-catch語句語法如下:
try{
//可能會發生異常的語句
} catch(Throwable e){
//處理異常e
}
- try程式碼塊
try程式碼塊中應該包含執行過程中可能會發生異常的語句。一條語句是否有可能發生異常,這要看語句中呼叫的方法。例如日期格式化類DateFormat的日期解析方法parse(),該方法的完整定義如下:
public Date parse(String source) throws ParseException
方法後面的throws ParseException說明:當呼叫parse()方法時有可以能產生ParseException異常。
提示 靜態方法、例項方法和構造方法都可以宣告丟擲異常,凡是丟擲異常的方法都可以通過try-catch進行捕獲,當然執行時異常可以不捕獲。一個方法宣告丟擲什麼樣的異常需要查詢API文件。
- catch程式碼塊
每個try程式碼塊可以伴隨一個或多個catch程式碼塊,用於處理try程式碼塊中所可能發生的多種異常。catch(Throwable e)語句中的e是捕獲異常物件,e必須是Throwable的子類,異常物件e的作用域在該catch程式碼塊中。
下面看看一個try-catch示例:
//HelloWorld.java檔案
package com.a51work6;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class HelloWorld {
public static void main(String[] args) {
Date date = readDate();
System.out.println("日期 = " + date);
}
// 解析日期
public static Date readDate() { ①
try {
String str = "2018-8-18"; //"201A-18-18"
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
// 從字串中解析日期
Date date = df.parse(str); ②
return date;
} catch (ParseException e) { ③
System.out.println("處理ParseException…");
e.printStackTrace(); ④
}
return null;
}
}
上述程式碼第①行定義了一個靜態方法用來將字串解析成日期,但並非所有的字串都是有效的日期字串,因此呼叫程式碼第②行的解析方法parse()有可能發生ParseException異常,ParseException是受檢查異常,在本例中使用try-catch捕獲。程式碼第③行的e就是ParseException物件。程式碼第④行e.printStackTrace()是列印異常堆疊跟蹤資訊,本例中的"2018-8-18"字串是有個有效的日期字串,因此不會發生異常。如果將字串改為無效的日期字串,如"201A-18-18",則會列印資訊。
處理ParseException
java.text.ParseException: Unparseable date: "201A-18-18"
日期 = null
at java.text.DateFormat.parse(Unknown Source)
at com.a51work6.HelloWorld.readDate(HelloWorld.java:24)
at com.a51work6.HelloWorld.main(HelloWorld.java:13)
提示 在捕獲到異常之後,通過e.printStackTrace()語句列印異常堆疊跟蹤資訊,往往只是用於除錯,給程式設計師提示資訊。堆疊跟蹤資訊對終端使用者是沒有意義的,本例中如果出現異常很有可能是使用者輸入的日期無效,捕獲到異常之後給使用者彈出一個對話方塊,提示使用者輸入日期無效,請使用者重新輸入,使用者重新輸入後再重新呼叫上述方法。這才是捕獲異常之後的正確處理方案。
多catch程式碼塊 {#catch}
如果try程式碼塊中有很多語句會發生異常,而且發生的異常種類又很多。那麼可以在try後面跟有多個catch程式碼塊。多catch程式碼塊語法如下:
try{
//可能會發生異常的語句
} catch(Throwable e){
//處理異常e
} catch(Throwable e){
//處理異常e
} catch(Throwable e){
//處理異常e
}
在多個catch程式碼情況下,當一個catch程式碼塊捕獲到一個異常時,其他的catch程式碼塊就不再進行匹配。
注意 當捕獲的多個異常類之間存在父子關係時,捕獲異常順序與catch程式碼塊的順序有關。一般先捕獲子類,後捕獲父類,否則子類捕獲不到。
示例程式碼如下:
//HelloWorld.java檔案
package com.a51work6;
……
public class HelloWorld {
public static void main(String[] args) {
Date date = readDate();
System.out.println("讀取的日期 = " + date);
}
public static Date readDate() {
FileInputStream readfile = null;
InputStreamReader ir = null;
BufferedReader in = null;
try {
readfile = new FileInputStream("readme.txt"); ①
ir = new InputStreamReader(readfile);
in = new BufferedReader(ir);
// 讀取檔案中的一行資料
String str = in.readLine(); ②
if (str == null) {
return null;
}
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date date = df.parse(str); ③
return date;
} catch (FileNotFoundException e) { ④
System.out.println("處理FileNotFoundException...");
e.printStackTrace();
} catch (IOException e) { ⑤
System.out.println("處理IOException...");
e.printStackTrace();
} catch (ParseException e) { ⑥
System.out.println("處理ParseException...");
e.printStackTrace();
}
return null;
}
}
上述程式碼通過Java I/O(輸入輸出)流技術從檔案readme.txt中讀取字串,然後解析成為日期。由於Java I/O技術還沒有介紹,讀者先不要關注I/O技術細節,這考慮呼叫它們的方法會發生異常就可以了。
在try程式碼塊中第①行程式碼呼叫FileInputStream構造方法可以會發生FileNotFoundException異常。第②行程式碼呼叫BufferedReader輸入流的readLine()方法可以會發生IOException異常。從圖14-1可見FileNotFoundException異常是IOException異常的子類,應該先FileNotFoundException捕獲,見程式碼第④行;後捕獲IOException,見程式碼第⑤行。
如果將FileNotFoundException和IOException捕獲順序調換,程式碼如下:
try{
//可能會發生異常的語句
} catch (IOException e) {
// IOException異常處理
} catch (FileNotFoundException e) {
// FileNotFoundException異常處理
}
那麼第二個catch程式碼塊永遠不會進入,FileNotFoundException異常處理永遠不會執行。
由於上述程式碼第⑥行ParseException異常與IOException和FileNotFoundException異常沒有父子關係,捕獲ParseException異常位置可以隨意放置。
try-catch語句巢狀 {#try-catch-0}
Java提供的try-catch語句巢狀是可以任意巢狀,修改14.3.2節示例程式碼如下:
//HelloWorld.java檔案
package com.a51work6;
… …
public class HelloWorld {
public static void main(String[] args) {
Date date = readDate();
System.out.println("讀取的日期 = " + date);
}
public static Date readDate() {
FileInputStream readfile = null;
InputStreamReader ir = null;
BufferedReader in = null;
try {
readfile = new FileInputStream("readme.txt");
ir = new InputStreamReader(readfile);
in = new BufferedReader(ir);
try { ①
String str = in.readLine(); ②
if (str == null) {
return null;
}
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date date = df.parse(str); ③
return date;
} catch (ParseException e) {
System.out.println("處理ParseException...");
e.printStackTrace();
} ④
} catch (FileNotFoundException e) { ⑤
System.out.println("處理FileNotFoundException...");
e.printStackTrace();
} catch (IOException e) { ⑥
System.out.println("處理IOException...");
e.printStackTrace();
}
return null;
}
}
上述程式碼第①~④行是捕獲ParseException異常try-catch語句,可見這個try-catch語句就是巢狀在捕獲IOException和FileNotFoundException異常的try-catch語句中。
程式執行時內層如果會發生異常,首先由內層catch進行捕獲,如果捕獲不到,則由外層catch捕獲。例如:程式碼第②行的readLine()方法可能發生IOException異常,該異常無法被內層catch捕獲,最後被程式碼第⑥行的外層catch捕獲。
注意 try-catch不僅可以巢狀在try程式碼塊中,還可以巢狀在catch程式碼塊或finally程式碼塊,finally程式碼塊後面會詳細介紹。try-catch巢狀會使程式流程變的複雜,如果能用多catch捕獲的異常,儘量不要使用try-catch巢狀。特別對於初學者不要簡單地使用Eclipse的語法提示不加區分地新增try-catch巢狀,要梳理好程式的流程再考慮try-catch巢狀的必要性。
多重捕獲 {#-0}
多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中多重捕獲異常用“|”運算子連線起來。
注意 有的讀者會問什麼不寫成FileNotFoundException | IOException | ParseException 呢?這是因為由於FileNotFoundException屬於IOException異常,IOException異常可以捕獲它的所有子類異常了。