????????大家好!今天我們來學習《Java 程序設計》中的第 12 章 —— 異常處理。在編程過程中,錯誤和異常是不可避免的。一個健壯的程序必須能夠妥善處理各種異常情況。本章將詳細介紹 Java 中的異常處理機制,幫助大家編寫出更穩定、更可靠的 Java 程序。
思維導圖
12.1 異常與異常類
12.1.1 異常的概念
????????在 Java 中,異常(Exception)?是指程序在運行過程中發生的非正常事件,它會中斷程序的正常執行流程。
????????想象一下現實生活中的場景:當你開車去上班時,可能會遇到輪胎漏氣、發動機故障等意外情況,這些情況會阻止你按計劃到達公司。在程序中也是如此,比如:
- 試圖打開一個不存在的文件
- 網絡連接中斷
- 除以零的運算
- 數組下標越界
????????這些情況都可以稱為異常。Java 的異常處理機制提供了一種優雅的方式來處理這些意外情況,使程序能夠繼續運行或友好地終止。
12.1.2 異常類
????????Java 中的所有異常都是通過類來表示的,這些類統稱為異常類。Java 提供了一個完善的異常類體系,所有異常類都直接或間接繼承自Throwable
類。
Throwable
類有兩個重要的子類:
- Error:表示嚴重的錯誤,通常是虛擬機相關的問題,如內存溢出(OutOfMemoryError),程序一般無法處理這類錯誤。
- Exception:表示程序可以處理的異常,是我們在編程中主要關注的類。
Exception
類又可以分為:
- Checked Exception(受檢異常):在編譯時就需要處理的異常,如果不處理,編譯器會報錯。如
IOException
、SQLException
等。 - Unchecked Exception(非受檢異常):也稱為運行時異常(RuntimeException),在編譯時不需要強制處理,通常是由程序邏輯錯誤引起的。如
NullPointerException
、ArrayIndexOutOfBoundsException
等。
下面是異常類的繼承關系圖:
@startuml
title Java異常類體系
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- RuntimeException
Exception <|-- IOException
Exception <|-- SQLException
RuntimeException <|-- NullPointerException
RuntimeException <|-- ArrayIndexOutOfBoundsException
RuntimeException <|-- ArithmeticException
@enduml
12.2 異常處理
????????Java 提供了一套完整的異常處理機制,主要通過try
、catch
、finally
、throw
和throws
關鍵字來實現。
12.2.1 異常的拋出與捕獲
異常處理的核心思想是拋出異常和捕獲異常:
- 拋出異常:當程序執行過程中遇到異常情況時,會創建一個異常對象并將其拋出。
- 捕獲異常:異常被拋出后,程序可以捕獲這個異常并進行處理,而不是讓程序直接崩潰。
????????形象地說,這就像生活中 "上報問題" 和 "解決問題" 的過程:員工遇到無法解決的問題(拋出異常),上報給經理(捕獲異常),經理來處理這個問題。
12.2.2 try-catch-finally 語句
try-catch-finally
是 Java 中處理異常的基本結構,語法如下:
try {// 可能會發生異常的代碼
} catch (異常類型1 異常對象名) {// 處理異常類型1的代碼
} catch (異常類型2 異常對象名) {// 處理異常類型2的代碼
} finally {// 無論是否發生異常,都會執行的代碼
}
- try 塊:包含可能會拋出異常的代碼。
- catch 塊:用于捕獲并處理 try 塊中拋出的異常。可以有多個 catch 塊,分別處理不同類型的異常。
- finally 塊:無論是否發生異常,都會執行的代碼,通常用于釋放資源。
執行流程示意圖:
示例代碼:
public class TryCatchFinallyDemo {public static void main(String[] args) {int a = 10;int b = 0;int[] arr = {1, 2, 3};try {// 可能發生異常的代碼int result = a / b; // 會拋出ArithmeticExceptionSystem.out.println("數組的第4個元素是:" + arr[3]); // 會拋出ArrayIndexOutOfBoundsExceptionSystem.out.println("計算結果:" + result);} catch (ArithmeticException e) {// 處理算術異常System.out.println("發生算術異常:" + e.getMessage());e.printStackTrace(); // 打印異常堆棧信息} catch (ArrayIndexOutOfBoundsException e) {// 處理數組下標越界異常System.out.println("發生數組下標越界異常:" + e.getMessage());} finally {// 無論是否發生異常,都會執行System.out.println("finally塊執行了,通常用于釋放資源");}System.out.println("程序繼續執行...");}
}
運行上述代碼,輸出結果:
12.2.3 用 catch 捕獲多個異常
Java 7 及以上版本允許在一個catch
塊中捕獲多種類型的異常,使用|
分隔不同的異常類型。
示例代碼:
import java.io.FileNotFoundException;
import java.io.IOException;public class MultiCatchDemo {public static void main(String[] args) {try {// 模擬可能拋出不同異常的操作int choice = Integer.parseInt(args[0]);if (choice == 1) {throw new FileNotFoundException("文件未找到");} else if (choice == 2) {throw new IOException("I/O操作失敗");} else if (choice == 3) {throw new ArithmeticException("算術錯誤");}} catch (FileNotFoundException e) {// 先捕獲子類異常System.out.println("處理文件未找到異常:" + e.getMessage());} catch (IOException e) {// 再捕獲父類異常System.out.println("處理其他I/O異常:" + e.getMessage());} catch (ArithmeticException e) {// 捕獲ArithmeticExceptionSystem.out.println("處理算術異常:" + e.getMessage());} catch (ArrayIndexOutOfBoundsException e) {// 捕獲數組下標越界異常(當沒有傳入命令行參數時)System.out.println("請傳入一個整數參數(1-3)");}System.out.println("程序結束");}
}
說明:
- 當一個
catch
塊捕獲多種異常類型時,這些異常類型不能有繼承關系。 - 這種寫法比多個
catch
塊更簡潔,尤其是當多種異常的處理邏輯相同時。 - 可以通過命令行參數來測試不同的異常情況:
java MultiCatchDemo 1
:測試 FileNotFoundExceptionjava MultiCatchDemo 2
:測試 IOExceptionjava MultiCatchDemo 3
:測試 ArithmeticExceptionjava MultiCatchDemo
:測試 ArrayIndexOutOfBoundsException
12.2.4 聲明方法拋出異常
????????如果一個方法可能會拋出異常,但不想在方法內部處理,而是讓調用者來處理,可以使用throws
關鍵字在方法聲明處聲明該方法可能拋出的異常。
語法:
修飾符 返回值類型 方法名(參數列表) throws 異常類型1, 異常類型2, ... {// 方法體
}
示例代碼:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class ThrowsDemo {// 聲明方法可能拋出FileNotFoundException和IOExceptionpublic static void readFile(String fileName) throws FileNotFoundException, IOException {FileInputStream fis = new FileInputStream(fileName);int data = fis.read();while (data != -1) {System.out.print((char) data);data = fis.read();}fis.close();}public static void main(String[] args) {try {// 調用聲明了拋出異常的方法,必須處理這些異常readFile("test.txt");} catch (FileNotFoundException e) {System.out.println("文件未找到:" + e.getMessage());} catch (IOException e) {System.out.println("文件讀取錯誤:" + e.getMessage());}System.out.println("\n程序執行完畢");}
}
說明:
- 對于 checked exception,如果方法不處理,就必須在方法聲明中用
throws
聲明。 - 對于 unchecked exception(RuntimeException 及其子類),可以不用
throws
聲明,編譯器不會強制要求。 - 調用聲明了異常的方法時,要么用
try-catch
處理這些異常,要么在當前方法中也用throws
聲明繼續向上拋出。
12.2.5 用 throw 語句拋出異常
??throw
語句用于手動拋出一個具體的異常對象。通常在滿足特定條件時,我們認為這是一個異常情況,就可以手動拋出異常。
語法:
throw 異常對象;
示例代碼:
public class ThrowDemo {// 計算年齡的方法,如果年齡不合法則拋出異常public static void printAge(int birthYear) {int currentYear = 2023;int age = currentYear - birthYear;if (birthYear < 1900 || birthYear > currentYear) {// 手動拋出異常throw new IllegalArgumentException("出生年份不合法:" + birthYear);}System.out.println("今年" + age + "歲");}public static void main(String[] args) {try {printAge(2000); // 合法的出生年份printAge(2050); // 不合法的出生年份,會拋出異常printAge(1850); // 這行代碼不會執行} catch (IllegalArgumentException e) {System.out.println("捕獲到異常:" + e.getMessage());}System.out.println("程序繼續執行");}
}
運行結果:
throw
與throws
的區別:
throw
用于方法內部,拋出的是一個具體的異常對象。throws
用于方法聲明處,聲明的是方法可能拋出的異常類型,可以是多個。
12.2.6 try-with-resources 語句
????????Java 7 引入了try-with-resources
語句,用于自動管理資源(如文件流、數據庫連接等)。它確保在資源使用完畢后自動關閉資源,無需在finally
塊中手動關閉。
要使用try-with-resources
,資源類必須實現AutoCloseable
接口(或其子類Closeable
接口)。
語法:
try (資源聲明) {// 使用資源的代碼
} catch (異常類型 異常對象名) {// 處理異常的代碼
}
示例代碼:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;public class TryWithResourcesDemo {public static void main(String[] args) {// try-with-resources語句,資源會自動關閉try (FileInputStream fis = new FileInputStream("test.txt")) {int data = fis.read();while (data != -1) {System.out.print((char) data);data = fis.read();}} catch (FileNotFoundException e) {System.out.println("文件未找到:" + e.getMessage());} catch (IOException e) {System.out.println("文件讀取錯誤:" + e.getMessage());}System.out.println("\n程序執行完畢");}
}
傳統方式與 try-with-resources 對比:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;// 傳統方式需要在finally中手動關閉資源
public class TraditionalResourceHandling {public static void main(String[] args) {FileInputStream fis = null;try {fis = new FileInputStream("test.txt");// 使用資源,這里添加一些實際的讀取操作示例int data = fis.read();while (data != -1) {System.out.print((char) data);data = fis.read();}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (fis != null) {try {fis.close(); // 關閉資源可能也會拋出異常} catch (IOException e) {e.printStackTrace();}}}}
}
說明:
try-with-resources
語句可以聲明多個資源,用分號分隔。- 資源的關閉順序與聲明順序相反。
try-with-resources
語句也可以有catch
和finally
塊,用于處理異常或執行必要的清理工作。
12.3 自定義異常類
????????Java 提供的異常類可能無法滿足所有業務需求,這時我們可以自定義異常類。自定義異常類通常繼承自Exception
(checked exception)或RuntimeException
(unchecked exception)。
自定義異常類的步驟:
- 創建一個類,繼承
Exception
或RuntimeException
。 - 提供構造方法,通常至少提供兩個構造方法:一個無參構造方法,一個帶有詳細信息的構造方法。
示例代碼:
// 自定義異常類:用戶年齡不合法異常
class InvalidAgeException extends Exception {// 無參構造方法public InvalidAgeException() {super();}// 帶有詳細信息的構造方法public InvalidAgeException(String message) {super(message);}
}// 自定義異常類:用戶姓名為空異常(繼承自RuntimeException)
class EmptyNameException extends RuntimeException {public EmptyNameException() {super();}public EmptyNameException(String message) {super(message);}
}// 使用自定義異常的示例
public class CustomExceptionDemo {// 注冊用戶的方法public static void registerUser(String name, int age) throws InvalidAgeException {if (name == null || name.trim().isEmpty()) {// 拋出unchecked異常,不需要在方法聲明中throwsthrow new EmptyNameException("用戶名不能為空");}if (age < 0 || age > 150) {// 拋出checked異常,需要在方法聲明中throwsthrow new InvalidAgeException("年齡不合法:" + age + ",年齡必須在0-150之間");}System.out.println("用戶注冊成功:" + name + "," + age + "歲");}public static void main(String[] args) {try {registerUser("張三", 25); // 正常情況registerUser("", 30); // 姓名為空,會拋出EmptyNameExceptionregisterUser("李四", 200); // 年齡不合法,會拋出InvalidAgeException} catch (InvalidAgeException e) {System.out.println("注冊失敗:" + e.getMessage());} catch (EmptyNameException e) {System.out.println("注冊失敗:" + e.getMessage());}System.out.println("程序結束");}
}
運行結果:
何時需要自定義異常:
- 當 Java 內置異常不能準確描述業務中的異常情況時。
- 希望通過異常類型來區分不同的錯誤場景,便于異常處理。
- 需要在異常中包含特定的業務信息時。
12.4 斷言
????????斷言(Assertion)是 Java 1.4 引入的特性,用于在程序開發和測試階段檢查某些條件是否滿足。如果斷言失敗,會拋出AssertionError
。
12.4.1 使用斷言
斷言的語法有兩種形式:
- 簡單形式
assert 布爾表達式;
如果布爾表達式的值為false
,則拋出AssertionError
。
? ? 2.帶消息的形式:
assert 布爾表達式 : 消息表達式;
如果布爾表達式的值為false
,則拋出AssertionError
,并將消息表達式的值作為錯誤消息。
12.4.2 開啟和關閉斷言
????????默認情況下,Java 虛擬機(JVM)是關閉斷言功能的。要開啟斷言,需要使用-ea
(或-enableassertions
)參數。
開啟斷言的方式:
- 對所有類開啟斷言:
java -ea 類名
- 對特定包開啟斷言:
java -ea:包名... 類名
- 對特定類開啟斷言:
java -ea:類名 類名
關閉斷言的方式:
- 使用
-da
(或-disableassertions
)參數,用法與-ea
類似。
在 IDE(如 Eclipse、IntelliJ IDEA)中,可以在運行配置中設置 VM 參數來開啟或關閉斷言。
12.4.3 何時使用斷言
斷言主要用于:
- 檢查程序內部的 invariants(不變量),即那些在程序正常執行時必須為真的條件。
- 檢查方法的前置條件和后置條件。
- 檢查私有方法的參數有效性(對于公共方法,應使用異常來處理無效參數)。
注意:
- 斷言不應該用于檢查程序運行時可能出現的預期錯誤,如用戶輸入錯誤。
- 斷言可能會被關閉,因此不能依賴斷言來處理程序的關鍵功能。
- 不要在斷言表達式中包含有副作用的操作(如修改變量值),因為當斷言關閉時,這些操作不會執行。
12.4.4 斷言示例
public class AssertionDemo {// 計算三角形面積的方法(海倫公式)public static double calculateTriangleArea(double a, double b, double c) {// 檢查前置條件:三角形的三條邊必須為正數assert a > 0 && b > 0 && c > 0 : "三角形的邊長必須為正數";// 檢查前置條件:三角形任意兩邊之和大于第三邊assert a + b > c && a + c > b && b + c > a : "不滿足三角形兩邊之和大于第三邊";double s = (a + b + c) / 2;double area = Math.sqrt(s * (s - a) * (s - b) * (s - c));// 檢查后置條件:面積必須為正數assert area > 0 : "計算出的面積必須為正數";return area;}public static void main(String[] args) {try {double area1 = calculateTriangleArea(3, 4, 5);System.out.println("直角三角形的面積:" + area1);// 測試無效的三角形(兩邊之和不大于第三邊)double area2 = calculateTriangleArea(1, 1, 3);System.out.println("面積:" + area2);} catch (AssertionError e) {System.out.println("斷言失敗:" + e.getMessage());}}
}
運行說明:
- 當關閉斷言運行時(默認情況),程序不會檢查斷言條件,可能會計算出不合理的結果。
- 當開啟斷言運行時(
java -ea AssertionDemo
),第二個計算會觸發斷言失敗,輸出:
直角三角形的面積:6.0
斷言失敗:不滿足三角形兩邊之和大于第三邊
12.5 小結
本章我們學習了 Java 中的異常處理機制,主要內容包括:
- 異常的概念:異常是程序運行時發生的非正常事件,會中斷程序的正常執行。
- 異常類體系:所有異常類都繼承自
Throwable
,主要分為Error
和Exception
兩大類。Exception
又分為 checked exception 和 unchecked exception。 - 異常處理機制:
- 使用
try-catch-finally
語句捕獲和處理異常。 - 使用
throws
聲明方法可能拋出的異常。 - 使用
throw
手動拋出異常。 - 使用
try-with-resources
自動管理資源。
- 使用
- 自定義異常:當 Java 內置異常不能滿足需求時,可以自定義異常類。
- 斷言:用于開發和測試階段檢查某些條件是否滿足,默認是關閉的。
????????掌握異常處理是編寫健壯 Java 程序的關鍵。合理地使用異常處理機制,可以使程序在遇到錯誤時能夠優雅地處理,而不是直接崩潰,同時也便于調試和維護。
編程練習
練習 1:除法計算器
編寫一個程序,實現兩個整數的除法運算。要求:- 處理除數為 0 的情況(ArithmeticException)。
- 處理輸入非整數的情況(InputMismatchException)。
- 使用 try-catch-finally 結構,確保程序在任何情況下都能友好地提示用戶。
練習 1 參考答案:
import java.util.InputMismatchException;
import java.util.Scanner;public class DivisionCalculator {public static void main(String[] args) {Scanner scanner = null;try {scanner = new Scanner(System.in);System.out.print("請輸入被除數:");int dividend = scanner.nextInt();System.out.print("請輸入除數:");int divisor = scanner.nextInt();int result = dividend / divisor;System.out.println(dividend + " / " + divisor + " = " + result);} catch (ArithmeticException e) {System.out.println("錯誤:除數不能為0");} catch (InputMismatchException e) {System.out.println("錯誤:請輸入有效的整數");} finally {if (scanner != null) {scanner.close();System.out.println("資源已釋放");}}System.out.println("程序結束");}
}
????????
????????希望本章的內容能幫助你理解和掌握 Java 異常處理的相關知識。如果有任何疑問或建議,歡迎在評論區留言討論!