Java 基礎語法 異常處理

前些章節的知識點有時會涉及到異常的知識,如果沒有專門學習過異常的小夥伴可能看的有點疑惑。今天這節就是為瞭講解異常,讓我們來瞭解什麼是異常,它的作用是啥,怎麼使用異常。

1. 異常的背景

1.1 邂逅異常

大傢在學習 Java 時,應該也遇見過一些異常瞭,例如

算術異常:

System.out.println(10 / 0);

結果為:Exception in thread "main" java.lang.ArithmeticException: / by zero

數組越界異常:

int[] arr = {1, 2, 3, 4, 5};
System.out.println(arr[100]);

結果為Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 100

空指針異常:

int[] arr = null;
System.out.println(arr.length);

結果為:Exception in thread "main" java.lang.NullPointerException

那麼什麼是異常呢?

異常是程序中的一些錯誤,但並不是所有的錯誤都是異常,並且錯誤有時候是可以避免的。

1.2 異常和錯誤

  • 異常被分為下面兩種

運行時異常(非受查異常):

在程序運行(通過編譯已經得到瞭字節碼文件,再由 JVM 執行)的過程當中發生的異常,是可能被大傢避免的異常,這些異常在編譯時可以被忽略。

例如:算數異常、空指針異常、數組越界異常等等

編譯時異常(受查異常):

編譯時發生的異常,這個異常是大傢難以預見的,這些異常在編譯時不能被簡單的忽略。

例如:要打開一個不存在的文件時,一個異常就發生瞭

除瞭異常我們我們也要瞭解下錯誤

錯誤:

錯誤不是異常,而是脫離程序員控制的問題,錯誤在代碼中通常被忽略。

例如:當棧溢出時,一個錯誤就發生瞭,這是編譯檢查不到的

public static void func(){
    func();
}
public static void main(String[] args){
    func();
}

結果為:Exception in thread "main" java.lang.StackOverflowError

那麼異常和錯誤的區別是什麼呢?

出現錯誤必須由我們程序員去處理它的邏輯錯誤,而出現異常我們隻要去處理異常就好瞭

如果有疑惑的夥伴通過後面的介紹你會逐漸瞭解它們的區別

1.3 Java 異常的體系(含體系圖)

Java 中異常的種類是很多的,我將一些異常收集並歸類如下

在這裡插入圖片描述

其中 Error 是錯誤,Exception 是異常。而異常中又分為瞭兩種,黃色的是編譯時異常,橙色的是運行時異常。

但是這張圖不僅僅說明瞭上述的關系,我們還要知道

每個異常其實都是一個類,並且箭頭代表瞭繼承的關系

我們可以通過一個代碼來理解

int[] arr = null;
System.out.println(arr.length);
// 結果為:Exception in thread "main" java.lang.NullPointerException

此時我們點擊這個異常 NullPointerException 就轉到瞭它的定義,我們會看到

在這裡插入圖片描述

我們可以得到以下結論:

  • NullPointerException 是一個類
  • 這個類繼承瞭 RuntimeException 這個類

為瞭刨根究底,我們繼續轉到 RuntimeException 這個類看看

在這裡插入圖片描述

我們又得到瞭以下結論:

  • RuntimeException 是一個類
  • 這個類繼承瞭 Exception 這個類

繼續刨根究底,我們又可以看到

誒,此時我們再對照著體系圖我們就可以理解清除這張圖的所有意思,並且此時對異常又有瞭個全面對認識

而今天我們的主角是異常,即 Exception,接下來我將會對它進行解析。

1.4 異常的核心思想

作為一個程序員,我們經常都面對著

在這裡插入圖片描述

錯誤在代碼中的存在我們不言而喻,因此就產生瞭兩種主要針對錯誤的方式

方式一(LBYL):在操作之前就做充分的檢查
方式二(EAFP):直接操作,有錯誤再解決

而異常的核心思想就是 EAFP

1.5 異常的好處

那麼核心思想為 EAFP 的異常有什麼好處呢?

我們可以隨便舉一個例子,比如你打一把王者,我們要進行登錄、匹配、確認遊戲、選擇英雄等等的操作。

如果使用 LBYL 風格的代碼,我們就要對每一步都做好充分的檢查之後,再進行下一步,簡單寫個代碼如下

boolean ret = false;
ret = log();
if(!=ret){
 // 處理登錄遊戲錯誤
 return;
}
ret = matching();
if(!=ret){
 // 處理匹配遊戲錯誤
 return;
}
ret = confirm();
if(!=ret){
 // 處理確認遊戲錯誤
 return;
}
ret = choose();
if(!=ret){
 // 處理選擇遊戲錯誤
 return;
}

而使用 EAFP 的風格,代碼則是這樣的

try{
    log();
    matching();
    confirm();
    choose();
}catch(登錄遊戲異常){
 // 處理登錄遊戲錯誤
}catch(匹配遊戲異常){
 // 處理匹配遊戲錯誤
}catch(確認遊戲異常){
 // 處理確認遊戲錯誤
}catch(選擇遊戲異常){
 // 處理選擇遊戲錯誤
}

兩種方式的代碼一對比,大傢也可以看得出哪一種更好。EAFP 風格的就可以將流程和處理異常的代碼分開,看起來更加舒服。而這也就是使用異常的好處之一。上述代碼運用瞭異常的基本用法,後續會介紹。

2. 異常的基本用法

2.1 捕獲異常

2.1.1 基本語法

try{
    // 有可能出現異常的語句
}[catch(異常類型 異常對象){
    // 出現異常後的處理行為
}...]
[finally{
    // 異常的出口
}]

  • try 代碼塊中放的是可能出現異常的代碼
  • catch 代碼塊中放的是出現異常後的處理行為
  • finally 代碼塊中的代碼用於處理善後工作,會在最後執行
  • 其中 catch finally 都可以根據情況選擇加或者不加

2.1.2 示例一

首先我們看一個不處理異常的代碼

int[] arr = {1, 2, 3};
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");

結果是:

在這裡插入圖片描述

我們分析一下這個結果,首先它告訴我們在 main 方法中出現瞭數組越界的異常,原因就是100這個數字。下面它又告訴我們瞭這個異常的具體位置。

並且通過這個結果我們知道,當代碼出現異常之後,程序就中止瞭,異常代碼後面的代碼就不會執行瞭。

那麼為什麼這裡拋出異常之後,後面的代碼就不再執行瞭呢?

因為當沒有處理異常的時候,一旦程序發生異常,這個異常就會交給 JVM 來處理。
而一旦交給瞭 JVM 處理異常,程序就會立即終止執行!

這也就是為什麼我們會有自己處理異常這個行為

我們如果加上 try catch 自己處理異常

int[] arr = {1, 2, 3};
try {
    System.out.println("before");
    System.out.println(arr[100]);
    System.out.println("after");
}catch(ArrayIndexOutOfBoundsException e){
    System.out.println("數組越界!");
}
System.out.println("after try catch");

結果是:

在這裡插入圖片描述

我們發現 try 中出現瞭異常的語句,並且我們針對這個異常做出瞭處理的行為。而 try catch 後面的程序依然可以繼續執行

我們在上述代碼中處理異常時 catch 裡面用的語句就是直接告訴它出現瞭什麼問題,但是如果我們想要知道這是什麼異常,在代碼的第幾行有問題的話,就可以再加一個調用棧。

什麼是調用棧呢?

方法之間存在相互調用關系,這種調用關系可以用“調用棧”來描述。在 JVM 中有一塊內存空間稱為“虛擬機棧”,這是專門存儲方法之間調用關系的。當代碼中出現異常的時候,我們就可使用 e.printStackTrace(); 來查看出現異常代碼的調用棧

2.1.3 示例二(含使用調用棧)

int[] arr = {1, 2, 3};
try {
    System.out.println("before");
    System.out.println(arr[100]);
    System.out.println("after");
}catch(ArrayIndexOutOfBoundsException e){
    System.out.println("數組越界!");
    e.printStackTrace();
}
System.out.println("after try catch");

結果是:

在這裡插入圖片描述

2.1.4 示例三(可以使用多個 catch 捕獲不同的異常)

int[] arr = {1, 2, 3};
try {
    System.out.println("before");
    System.out.println(arr[100]);
    System.out.println("after");
}catch(ArrayIndexOutOfBoundsException e){
    System.out.println("數組越界!");
    e.printStackTrace();
}catch(NullPointerException e){
    System.out.println("空指針異常");
    e.printStackTrace();
}
System.out.println("after try catch");

這個代碼裡面有多個 catch,他會捕獲到第一個出現異常的位置

2.1.5 示例四(可以使用一個 catch 捕獲所有異常,不推薦)

int[] arr = {1, 2, 3};
try {
    System.out.println("before");
    System.out.println(arr[100]);
    System.out.println("after");
}catch(Exception e){
    e.printStackTrace();
}
System.out.println("after try catch");

其中我們使用瞭 Exception 這個類,我們知道它是所有異常的父類,因此可以用來捕獲所有異常。但是這個方法是不推薦的,因為異常太多瞭,我們不容易定位問題

並且我們能得到一個結論

catch 進行類型匹配的時候,不光會匹配相同類型的異常,也能捕獲目標異常類型的子類對象

2.1.6 示例五(使用 finally,它之間的代碼將在 try 語句後執行)

int[] arr = {1, 2, 3};
try {
    arr = null;
    System.out.println(arr.length);
}catch(NullPointerException e){
    e.printStackTrace();
}finally{
    System.out.println("finally 執行啦!");
}
System.out.println("after try catch");

結果為:

在這裡插入圖片描述

我們緊接著再看一個代碼,我將異常給改正確

int[] arr = {1, 2, 3};
try {
    System.out.println(arr.length);
}catch(NullPointerException e){
    e.printStackTrace();
}finally{
    System.out.println("finally 執行啦!");
}
System.out.println("after try catch");

上述代碼就沒有錯誤瞭,但是結果是

在這裡插入圖片描述

我們就得出瞭這個結論

無論 catch 是否捕獲到異常,都要執行 finally 語句

finally 是用來處理善後工作的,例如釋放資源是可以被做到的。如果大傢對於使用 finally 釋放資源有疑惑,可以先看示例八,因為在 finally 中加入 Scanner close 方法就是釋放資源的一種例子

2.1.7 示例六(finally 引申的思考題)

public static int func(){
    try{
        return 10;
    }catch(NullPointerException e){
        e.printStackTrace();
    }finally{
        return 1;
    }
}
public static void main(String[] args) {
    int num = func();
    System.out.println(num);
}

結果為:1

因為 finally 塊永遠是最後執行的。並且你也無法在這個代碼之後執行其他語句,因為不管有沒有捕獲到異常都要執行 finally 中的 return 語句去終止代碼

2.1.8 示例七(使用 try 負責回收資源)

在演示代碼前要先補充一個關於 Scanner 的知識

我們知道使用 Scanner 類可以幫助我們進行控制臺輸入語句,但是 Scanner 還是一種資源,而資源使用完之後是需要回收的,就像是我們打開瞭一瓶水喝瞭點還要蓋上它。故用完後我們可以加上 close 方法來進行回收,如

Scanner reader = new Scanner(System.in);
int a = reader.nextInt();
reader.close();

而 try 有一種寫法可以在它執行完畢後自動調用 Scanner close 方法

try(Scanner sc = new Scanner(System.in){
 int num = sc.nextInt();
}catch(InputMismatchException e){
 e.printStackTrace();
}

而這種方式的代碼風格要比使用 finally 中含有 close 方法要好些

2.1.9 示例八(本方法中沒有合適的處理異常方式,就會沿著調用棧向上傳遞)

public static void func(){
    int[] arr = {1, 2, 3};
    System.out.println(arr[100]);
}
public static void main(String[] args){
    try{
        func();
    }catch(ArrayIndexOutOfBoundsException e){
        e.printStackTrace();
    }
}

結果為:

在這裡插入圖片描述

由於我們寫 func 方法時出現瞭異常沒有及時處理,但我們在 main 方法中調用它瞭,所以就經過方法之間互相的調用關系,我們一直到瞭 main 方法被調用的位置,並且此時有合適的處理異常的方法

若最終沒有找到合適的異常處理方法,最終該異常就會交給 JVM 處理,即程序就會終止

2.1.10 異常處理流程總結

  • 程序先執行 try 中的代碼
  • 如果 try 中的代碼出現異常,就會結束 try 中異常之後的代碼,並查看該異常和 catch 中的異常類型是否匹配
  • 如果匹配,就會執行 catch 中的代碼
  • 如果沒有匹配的,就會將異常向上傳遞到上層調用者
  • 無論是否找到匹配類型,finally 中的代碼都會被執行
  • 如果上層調用者沒有處理異常的方法,就會繼續向上傳遞
  • 一直到 main 方法也沒有合適的代碼處理異常,就會交給 JVM 來處理,此時程序就會終止

2.2 拋出異常

以上我們介紹的都是 Java 內置的類拋出的一些異常,除此之外我們也可以使用關鍵字 throw 手動拋出一個異常,如

public static int divide(int x, int y) { 
    if (y == 0) { 
        throw new ArithmeticException("拋出除 0 異常"); 
    }
}
public static void main(String[] args) { 
 System.out.println(divide(10, 0)); 
} 

該代碼就是我們手動拋出的異常,並且手動拋出的異常還可以使用自定義的異常,後面將會介紹到

2.3 異常說明

我們在處理異常時,如果有一個方法,裡面很長一大段,我們其實是希望很簡單的就知道這段代碼有可能會出現哪些異常。故我們可以使用關鍵字 throws,把可能拋出的異常顯示的標註在方法定義的位置,從而提醒使用者要註意捕獲這些異常,如

public static int divide(int x, int y) throws ArithmeticException{ 
    if (y == 0) { 
        throw new ArithmeticException("拋出除 0 異常"); 
    }
}

註意:

如果我們將 main 方法拋出一個異常說明,而 main 方法的調用者是 JVM,所以如果在 main 函數上拋出異常的話,就相當於 JVM 來處理這個異常瞭

3. 自定義異常類

Java 中雖然有豐富的異常類,但是實際上肯定還要一些情況需要我們對這些異常進行擴展,創建新的符合情景的異常。

那怎麼創建自定義異常呢?首先我們就可以去看看原有的那些異常是怎麼做的

在這裡插入圖片描述

在這裡插入圖片描述

兩異常

我們發現這兩個異常都是繼承在 RuntimeException 這個類的,並且都構造瞭兩個構造方法,分別是不帶參數和帶參數

而我模擬瞭一個登錄賬號的代碼

public class TestDemo { 
    private static String userName = "root"; 
    private static String password = "123456"; 
    public static void main(String[] args) { 
        login("admin", "123456"); 
    } 
    public static void login(String userName, String password) { 
        if (!TestDemo.userName.equals(userName)) { 
            // 處理用戶名錯誤
        } 
        if (!TestDemo.password.equals(password)) { 
            // 處理密碼錯誤
        } 
        System.out.println("登陸成功"); 
    } 
}

通過這個模擬的場景,我們可以針對運行時賬號和密碼是否正確寫一個異常

class UserException extends RuntimeException{
    public UserException(){
        super();
    }
    public UserException(String s){
        super(s);
    }
}
class PasswordException extends RuntimeException{
    public PasswordException(){
        super();
    }
    public PasswordException(String s){
        super(s);
    }
}

緊接著我們再手動拋出異常

public class TestDemo { 
    private static String userName = "root"; 
    private static String password = "123456"; 
    public static void main(String[] args) { 
        login("admin", "123456"); 
    } 
    public static void login(String userName, String password) { 
        if (!TestDemo.userName.equals(userName)) { 
            throws new UserException("用戶名錯誤");
        } 
        if (!TestDemo.password.equals(password)) { 
            throws new PasswordException("密碼錯誤");
        } 
        System.out.println("登陸成功"); 
    } 
}

所以我們創建新的異常時,就是先思考這是哪種類型的異常,再照貓畫虎。但是可能有疑惑,如果我們新建異常時統一繼承 Exception 不就行嗎?

No!由於 Exception 分為編譯時異常和運行時異常,使用 Exception 的話默認是編譯時異常(即受查異常),而一段代碼可能拋出受查異常則必須顯示進行處理。

故如果我們將上述新建的異常繼承 Exception 的話,就要再對代碼中的異常進行處理,否則會直接報錯

到此這篇關於Java 基礎語法 異常處理的文章就介紹到這瞭,更多相關Java 異常處理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: