本文是Java基礎課程的第十課。主要介紹Java中的異常處理機制,包括異常與錯誤、常見異常、try…catch語句塊、throw\throws關鍵字、自定義異常等內容
文章目錄
一、Java中的異常與錯誤
1、什麼是異常與錯誤
大多數情況下,程序運行過程中不可能一帆風順,不可避免的會出現一些非正常的現象,比如用戶輸入了非法數據、程序要讀取的文件並不存在、某一步運算出現除數是0的情況、訪問的數組下標越界了、網絡臨時中斷、甚至內存不夠用而產生內存溢出等等。引起這些非正常現象的原因不一而足,有的是因爲用戶錯誤引起,有的是程序錯誤引起的,還有其它一些是因爲物理錯誤引起的。如果程序中出現了非正常的現象而沒有得到及時的處理,程序可能會掛起、自動退出、甚至崩潰,程序的健壯性會大大降低。
在Java中,這些非正常現象可以分爲異常和錯誤。
異常一般指在程序運行過程中,可以預料的非正常現象,異常一般是開發人員可控的,異常可以、也應當在程序中被捕獲並進行相應的處理,以保證程序的健壯。
錯誤一般指在程序運行過程中,不可預料的非正常現象,錯誤對於程序來說往往是致命的,一般是開發人員很難處理、無法控制的,因此也不需要開發人員進行處理。
2、相關類繼承關係
由於異常和錯誤總是難免的,良好的應用程序除了具備用戶所要求的基本功能外,還應該具備準確定義並描述錯誤和異常,及預見並處理可能發生的各種異常的能力。Java定義了一系列用以描述錯誤和異常的類,並且引進了一套用以捕獲、拋出、處理異常的機制。
在Java中,異常和錯誤都直接或間接繼承自Throwable
類,Throwable
類有兩個直接派生類,分別是Error
類和Exception
類,Error
類及其派生類用來描述錯誤,Exception
類及其派生類用來描述異常。下面是圖示:
錯誤一般發生在嚴重故障時。虛擬機會捕獲錯誤、實例化相應Error
類的派生類對象並拋出。通常發生錯誤的情況脫離開發人員的控制,也無法預料,所以在開發過程中通常不用刻意考慮。但開發人員應該認識一些可能會遇到的Error
類的派生類,方便在發生錯誤時定位、理解所發生的問題。
異常由Exception
類及其派生類來表示,Java中的異常也可以分成兩部分,一部分是檢查性異常,一部分是運行時異常。具體說明如下:
- 檢查性異常:除了
RuntimeException
類及其派生類所代表的異常之外,其他Exception
類的派生類所代表的異常都是檢查性異常。檢查性異常在編譯時不能簡單的忽略,必須在源碼中進行捕獲處理,這是編譯檢查的一部分。檢查性異常也被稱作設計時異常。 - 運行時異常:
RuntimeException
類及其派生類所代表的異常都是運行時異常。運行時異常是可以通過開發人員的努力而避免的。與檢查性異常相反的是,運行時異常可以在編譯時忽略。運行時異常也被稱作非檢查性異常。
二、異常
前文中已經提到,Java中的異常分爲檢查性異常與運行時異常,檢查性異常編譯時不能忽略,強制要求開發人員在開發階段捕獲處理;運行時異常不強制要求在代碼中捕獲並處理,但開發人員應在編碼過程中仔細完善代碼邏輯,儘量避免運行時異常的發生。
下面是一個代碼邏輯錯誤而導致運行時異常的示例:
package com.codeke.java.test;
public class Test {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
System.out.println("begin");
int result = num1 / num2;
System.out.println("end");
}
}
執行輸出結果:
begin
Exception in thread "main" java.lang.ArithmeticException: / by zero
at com.codeke.java.test.Test.main(Test.java:8)
說明:
- 本例中,變量
num2
的值爲0
,當num2
作爲除數時,出現ArithmeticException
異常,程序退出,語句System.out.println("end")
不再執行。
1、JDK中常見的異常類
JDK中已經內置了很多異常類,開發人員在開發調式代碼的過程中應該逐步認識並熟悉它們。
下面是一些JDK中常見的代表運行時異常的類:
異常類名 | 說明 |
---|---|
ArithmeticException |
當出現異常的運算條件時,拋出此異常。例如,一個整數"除以零" |
ArrayIndexOutOfBoundsException |
用非法索引訪問數組時拋出的異常。如果索引爲負或大於等於數組大小,則該索引爲非法索引 |
ArrayStoreException |
試圖將錯誤類型的對象存儲到一個對象數組時拋出的異常 |
ClassCastException |
當試圖將對象強制轉換爲不是實例的子類時,拋出該異常 |
IllegalArgumentException |
拋出的異常表明向方法傳遞了一個不合法或不正確的參數 |
IllegalThreadStateException |
線程沒有處於請求操作所要求的適當狀態時拋出的異常 |
IndexOutOfBoundsException |
指示某排序索引(例如對數組、字符串或向量的排序)超出範圍時拋出 |
NullPointerException |
當應用程序試圖在需要對象的地方使用 null 時,拋出該異常 |
NumberFormatException |
當應用程序試圖將字符串轉換成一種數值類型,但該字符串不能轉換爲適當格式時,拋出該異常 |
StringIndexOutOfBoundsException |
此異常由字符串方法拋出,指示索引或者爲負,或者超出字符串的大小 |
UnsupportedOperationException |
當不支持請求的操作時,拋出該異常 |
下面是一些JDK中常見的代表檢查性異常的類:
異常類名 | 說明 |
---|---|
FileNotFoundException |
文件操作時,找不到文件,拋出該異常 |
ClassNotFoundException |
應用程序試圖加載類時,找不到相應的類,拋出該異常 |
IllegalAccessException |
拒絕訪問一個類的時候,拋出該異常 |
NoSuchFieldException |
請求的字段不存在,拋出該異常 |
NoSuchMethodException |
請求的方法不存在,拋出該異常 |
InterruptedException |
一個線程被另一個線程中斷,拋出該異常 |
2、捕獲和處理異常
2.1、try…catch語句塊
在Java代碼中,使用try...catch
語句塊可以捕獲異常並進行處理,try...catch
語句塊放在異常可能發生的地方,try...catch
語句塊中的代碼稱爲保護代碼。使用try...catch
語句塊的語法如下:
try {
// 程序代碼
} catch (ExceptionName e) {
// catch 塊
}
說明:
- 可能發生異常的程序代碼放在
try
語句塊中。 catch
關鍵字後面緊跟的()
中包含要捕獲異常類型的聲明,catch
語句塊中包含的代碼一般爲對異常的處理。- 程序運行過程中,如果
try
語句塊內的代碼沒有出現任何異常,後面的catch
語句塊不執行;而當try
語句塊內的代碼發生一個異常時,try
語句塊中的後續代碼不再執行,系統會實例化一個該異常對應的異常類對象,後面的catch
語句塊會被檢查,如果該異常類對象 is acatch
關鍵字後面所聲明異常類的對象,該對象會被傳遞到catch
語句塊中,該catch
語句塊中的代碼將被執行。
下面是一個示例:
package com.codeke.java.test;
public class Test {
public static void main(String[] args) {
int num1 = 10;
int num2 = 0;
System.out.println("begin");
// 使用try...catch 包裹可能發生異常的代碼
try {
int result = num1 / num2;
} catch (ArithmeticException e) {
e.printStackTrace(); // 對異常的處理
}
// 後續代碼仍將得到執行
System.out.println("end");
}
}
執行輸出結果:
begin
java.lang.ArithmeticException: / by zero
at com.codeke.java.test.Test.main(Test.java:10)
end
說明:
- 本例中,用
try
語句塊包裹了可能出現異常的代碼int result = num1 / num2
。 - 當
try
語句塊中出現算術運算異常時,系統實例化了一個ArithmeticException
類的對象,並檢查該對象是否 is acatch
關鍵字後面所聲明異常類型的對象,如果是,將對象傳入catch
語句塊。 - 在
catch
語句塊中,異常類對象e
調用了printStackTrace()
方法,該方法可以向控制檯打印異常信息。 - 異常被捕獲處理後,程序沒有退出,
catch
語句塊之後的後續代碼得以執行,本例中語句System.out.println("end")
被執行。
在異常發生時,所有的異常信息都被封裝成爲一個個異常類的對象,異常類從Throwable
類繼承了一些常用的方法,用以獲取異常信息,下面列出異常類常用的API:
方法 | 返回值類型 | 方法說明 |
---|---|---|
getMessage() |
String |
返回關於發生的異常的詳細信息。這個消息在Throwable 類的構造函數中初始化了 |
getCause() |
Throwable |
返回一個Throwable 對象代表異常原因 |
printStackTrace() |
void |
打印toString()結果和棧層次到System.err,即錯誤輸出流 |
getStackTrace() |
StackTraceElement [] |
返回一個包含堆棧層次的數組。下標爲0的元素代表棧頂,最後一個元素代表方法調用堆棧的棧底 |
fillInStackTrace() |
Throwable |
用當前的調用棧層次填充Throwable 對象棧層次,添加到棧層次任何先前信息中 |
toString() |
String |
使用getMessage()的結果返回類的串級名字 |
2.2、多重 catch
一個 try
語句塊後面可以跟隨多個 catch
語句塊,用於對try
語句塊中可能發生的多個異常進行捕獲,這種情況也被稱作多重捕獲。
使用多重catch
語句塊的語法如下:
try {
// 程序代碼
} catch (ExceptionName1 e1){
// catch 塊1
} catch (ExceptionName2 e2){
// catch 塊2
} catch (ExceptionName3 e3){
// catch 塊3
}
在有多重catch
語句塊的情況下,如果try
語句塊中發生異常,try
語句塊中的後續代碼不再執行,系統會實例化一個相應異常類型的對象,並檢查從上往下第一個catch
關鍵字後面聲明的異常類型,符合 is a 關係時,將對象傳入catch
語句塊,否則繼續往下檢查第二個catch
關鍵字後面聲明的異常類型,直到找到對應的catch
語句塊或通過所有的catch
語句塊爲止。
下面是一個針對多個可能發生的檢查性異常,使用多重catch
的示例:
package com.codeke.java.test;
import java.io.*;
public class Test {
public static void main(String[] args) {
System.out.println("begin");
// 實例化file對象
File file = new File("D:\\test.txt");
try {
// 獲取file對象的輸入流
FileInputStream in = new FileInputStream(file);
// 讀取輸入流中的第一個字節
int i = in.read();
} catch (FileNotFoundException e) { // 第一個catch語句塊
e.printStackTrace();
} catch (IOException e) { // 第二個catch語句塊
e.printStackTrace();
}
System.out.println("end");
}
}
執行輸出結果:
begin
java.io.FileNotFoundException: D:\test.txt (系統找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at com.codeke.java.test.Test.main(Test.java:12)
end
說明:
- 本例中,語句
FileInputStream in = new FileInputStream(file)
可能會發生FileNotFoundException
,語句int i = in.read()
可能發生IOException
,針對可能發生的這兩個異常,使用了兩個catch
語句塊。 - 如果要捕獲的異常類之間沒有繼承關係,各類的
catch
語句塊順序無關緊要,但當它們之間有繼承關係時,應該將派生類的catch
語句塊放在基類的catch
語句塊之前。本例中,FileNotFoundException
爲IOException
的派生類,故應該寫在前面。
多重捕獲也可以合併寫在一個catch
語句塊中,語法如下:
try {
// 程序代碼
} catch (ExceptionName1 | ExceptionName2 [| ExceptionName3 ... | ExceptionNameN] e){
// catch 塊
}
需要注意的是,這種寫法僅限於要捕獲的各異常類之間沒有繼承關係的情況。後續章節的代碼中會出現這種情況,這裏不再舉例。
2.3、finally語句塊
在try...catch
語句塊後,可以使用finally
語句塊,無論try
語句塊中的代碼是否發生異常,finally
語句塊中的代碼總是會被執行,也因此,finally
語句塊中適合進行清理、回收資源等收尾善後性質的工作。
在try...catch
語句塊後跟隨finally
語句塊需要使用finally
關鍵字,語法如下:
try {
// 程序代碼
} catch (ExceptionName1 e1){
// catch 塊1
} catch (ExceptionName2 e2){
// catch 塊2
} finally {
// 必須執行的代碼,適合收尾、善後等
}
下面是一個示例:
package com.codeke.java.test;
import java.io.*;
public class Test {
public static void main(String[] args) {
System.out.println("begin");
// 實例化file對象
File file = new File("D:\\test.txt");
FileInputStream in = null;
try {
// 獲取file對象的輸入流
in = new FileInputStream(file);
// 讀取輸入流中的第一個字節
int i = in.read();
} catch (FileNotFoundException e) { // 第一個catch語句塊
e.printStackTrace();
} catch (IOException e) { // 第二個catch語句塊
e.printStackTrace();
} finally { // finally語句塊
try {
if (in != null) {
in.close(); // 關閉輸入流,這個操作本身也可能發生IOException,要求強制檢查
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("finally");
}
System.out.println("end");
}
}
執行輸出結果:
begin
java.io.FileNotFoundException: D:\test.txt (系統找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at com.codeke.java.test.Test.main(Test.java:13)
finally
end
說明:
- 本例完善了上一示例,在獲取輸入輸出流,進行完讀寫操作後,應當將輸入輸出流關閉,故在本例中,使用了
finally
語句塊,無論是否發生異常,finally
語句塊中的語句in.close()
都會將輸入流關閉。 - 細心的開發者可能會考慮這樣一個問題:如果不使用
finally
語句塊,而是直接將finally
語句塊中的語句放在catch
語句塊外的後續代碼中,無論try
語句塊中是否發生異常,這些語句不是仍然會執行嗎?finally
語句塊又有什麼使用的必要呢?事實上,考慮try
語句塊或catch
語句塊中有return
語句的情況,catch
語句塊外的後續代碼不一定能得到執行的機會,而就算try
語句塊或catch
語句塊中有return
語句,finally
語句塊中的代碼仍然會被執行,甚至,如果finally
語句塊中也有return
語句時,會覆蓋try
語句塊或catch
語句塊中的返回值,因此,使用finally
語句塊來執行收尾善後工作是必要的,也是開發人員應該養成的一個良好的編碼習慣。
3、拋出異常
3.1、throw關鍵字
通常,異常是自動拋出的。但開發人員也可以通過throw
關鍵字拋出異常。throw
語句拋出異常的語法格式如下:
throw new 異常類名([異常描述]);
下面是一個示例:
package com.codeke.java.test;
import java.util.Scanner;
/**
* 年滿18週歲可報考駕校,如果年齡不滿18週歲不允許包括駕校。
* 從鍵盤上輸入年齡,如果年齡不足18歲,拋出異常
*/
public class Test {
public static void main(String[] args) {
System.out.println("請輸入年齡:");
int age = new Scanner(System.in).nextInt();
validateAge(age);
System.out.println("年齡超過18歲,允許報考駕校");
}
/**
* 校驗年齡是否不足18歲的方法
* @param age 要檢驗的年齡
*/
public static void validateAge(int age){
if(age < 18){
throw new RuntimeException("年齡不足18歲,不允許考駕校");
}
}
}
執行輸出結果:
請輸入年齡:
16
Exception in thread "main" java.lang.RuntimeException: 年齡不足18歲,不允許考駕校
at com.codeke.java.test.Test.validateAge(Test.java:24)
at com.codeke.java.test.Test.main(Test.java:14)
說明:
- 本例的
validateAge(int age)
方法中,當傳入的參數age
不足18時,由開發人員實例化了一個運行時異常,並使用throw
關鍵字將該異常對象拋出。
3.2、throws關鍵字
對於需要捕獲的異常(基本上是檢查性異常),如果一個方法中沒有捕獲,調用該方法的主調方法應該捕獲並處理該異常。爲了明確某個方法不捕獲某個異常,而讓調用該方法的主調方法捕獲異常,可以在方法聲明的時候,使用throws
關鍵字拋出該類異常。在方法聲明中拋出某類型異常的語法如下:
[修飾符] 返回值類型 方法名([參數列表]) throws 異常類型名 {
// 方法體
}
下面是一個示例:
package com.codeke.java.test;
import java.io.*;
public class Test {
public static void main(String[] args) {
try {
System.out.println("main start");
readFile();
System.out.println("main end");
} catch (IOException e) {
e.printStackTrace();
System.out.println("main catched");
}
System.out.println("over");
}
/**
* 讀取文件
* @throws IOException IO異常
*/
public static void readFile() throws IOException {
File file = new File("D:\\test.txt");
// 獲取file對象的輸入流
FileInputStream in = new FileInputStream(file);
// 讀取輸入流中的第一個字節
int i = in.read();
}
}
執行輸出結果:
main start
java.io.FileNotFoundException: D:\test.txt (系統找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at com.codeke.java.test.Test.readFile(Test.java:26)
at com.codeke.java.test.Test.main(Test.java:10)
main catched
over
說明:
- 本例中,
readFile()
方法中可能出現FileNotFoundException
和IOException
,但在readFile()
方法並不想直接捕獲處理這些異常,故可以在方法聲明時使用throws
關鍵字拋出異常給主調方法(由於FileNotFoundException
是IOException
的派生類,故拋出IOException
即可),此時,在主調方法中仍然需要捕獲並處理被調方法拋出的異常。
4、自定義異常
系統定義的異常不能代表應用程序中所有的異常,有時開發人員需要聲明自定義異常。聲明自定義異常非常簡單,將系統定義的異常類作爲基類,聲明派生類即可。一般在聲明自定義異常時,會選擇繼承Exception
類或RuntimeException
類。從Exception
類繼承的自定義異常是檢查性異常,在應用程序中必須使用try...catch
語句塊捕獲並處理;不過自定義異常一般是可控的異常,大部分情況下不需要捕獲,因此讓自定義異常直接繼承自RuntimeException
類是開發人員更多情況下的選擇。
下面是一個示例:
InputException
類的源碼:
package com.codeke.java.test;
/**
* 輸入異常
*/
public class InputException extends RuntimeException {
public InputException(String message) {
super(message);
}
}
測試類Test
類的源碼:
package com.codeke.java.test;
import java.util.Scanner;
/**
* 校驗輸入的姓名不爲空且長度是否不小於6位
*/
public class Test {
public static void main(String[] args) {
System.out.println("請輸入用戶名");
String name = new Scanner(System.in).next();
validateName(name);
}
/**
* 校驗姓名是否存在並且長度不小於6位
* @param name 要校驗的姓名
*/
public static void validateName(String name) {
if (name == null || name.length() < 6) {
throw new InputException("用戶名必須填寫,長度不小於6位");
}
}
}
執行輸出結果:
請輸入用戶名:
tom
Exception in thread "main" com.codeke.java.test.InputException: 用戶名必須填寫,長度不小於6位
at com.codeke.java.test.Test.validateName(Test.java:22)
at com.codeke.java.test.Test.main(Test.java:13)
說明:
- 本例中,創建了一個自定義異常
InputException
,它繼承自RuntimeException
類,故是一個運行時異常,不強制要求使用try...catch
語句塊捕獲並處理。 - 在本例的
validateName(String name)
方法中,當傳入的參數name
爲空或長度小於6位時,實例化了一個自定義異常,即InputException
類的對象,並使用throw
關鍵字將該異常對象拋出。