JAVA學習筆記 10 - 異常和錯誤

本文是Java基礎課程的第十課。主要介紹Java中的異常處理機制,包括異常與錯誤、常見異常、try…catch語句塊、throw\throws關鍵字、自定義異常等內容

一、Java中的異常與錯誤

1、什麼是異常與錯誤

大多數情況下,程序運行過程中不可能一帆風順,不可避免的會出現一些非正常的現象,比如用戶輸入了非法數據、程序要讀取的文件並不存在、某一步運算出現除數是0的情況、訪問的數組下標越界了、網絡臨時中斷、甚至內存不夠用而產生內存溢出等等。引起這些非正常現象的原因不一而足,有的是因爲用戶錯誤引起,有的是程序錯誤引起的,還有其它一些是因爲物理錯誤引起的。如果程序中出現了非正常的現象而沒有得到及時的處理,程序可能會掛起、自動退出、甚至崩潰,程序的健壯性會大大降低。

在Java中,這些非正常現象可以分爲異常錯誤

異常一般指在程序運行過程中,可以預料非正常現象,異常一般是開發人員可控的,異常可以、也應當在程序中被捕獲並進行相應的處理,以保證程序的健壯。

錯誤一般指在程序運行過程中,不可預料非正常現象,錯誤對於程序來說往往是致命的,一般是開發人員很難處理無法控制的,因此也不需要開發人員進行處理。

2、相關類繼承關係

由於異常和錯誤總是難免的,良好的應用程序除了具備用戶所要求的基本功能外,還應該具備準確定義並描述錯誤和異常,及預見並處理可能發生的各種異常的能力。Java定義了一系列用以描述錯誤和異常的類,並且引進了一套用以捕獲、拋出、處理異常的機制。

在Java中,異常錯誤直接或間接繼承Throwable類,Throwable類有兩個直接派生類,分別是Error類和Exception類,Error類及其派生類用來描述錯誤Exception類及其派生類用來描述異常。下面是圖示:
在這裏插入圖片描述
錯誤一般發生在嚴重故障時。虛擬機會捕獲錯誤、實例化相應Error類的派生類對象並拋出。通常發生錯誤的情況脫離開發人員控制,也無法預料,所以在開發過程中通常不用刻意考慮。但開發人員應該認識一些可能會遇到的Error類的派生類,方便在發生錯誤時定位、理解所發生的問題。

異常Exception類及其派生類來表示,Java中的異常也可以分成兩部分,一部分是檢查性異常,一部分是運行時異常。具體說明如下:

  1. 檢查性異常除了RuntimeException類及其派生類所代表的異常之外,其他Exception類的派生類所代表的異常都是檢查性異常。檢查性異常在編譯時不能簡單的忽略必須在源碼中進行捕獲處理,這是編譯檢查的一部分。檢查性異常也被稱作設計時異常
  2. 運行時異常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語句塊之前。本例中,FileNotFoundExceptionIOException的派生類,故應該寫在前面。

多重捕獲也可以合併寫在一個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()方法中可能出現FileNotFoundExceptionIOException,但在readFile()方法並不想直接捕獲處理這些異常,故可以在方法聲明時使用throws關鍵字拋出異常給主調方法(由於FileNotFoundExceptionIOException的派生類,故拋出IOException即可),此時,在主調方法中仍然需要捕獲並處理被調方法拋出的異常。

4、自定義異常

系統定義異常不能代表應用程序中所有異常有時開發人員需要聲明自定義異常。聲明自定義異常非常簡單,將系統定義的異常類作爲基類聲明派生類即可。一般在聲明自定義異常時,會選擇繼承ExceptionRuntimeException。從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關鍵字將該異常對象拋出。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章