Java之異常詳解

一、異常的概念

異常就是在程序運行過程中所發生的的不正常的事件,它會中斷正在運行的程序。
在這裏插入圖片描述
在生活中,我們會根據不同的異常進行相應的處理,而不會就此中斷我們的生活。
在這裏插入圖片描述


二、異常處理機制

Java的異常處理機制可以讓程序具有極好的容錯性,讓程序更加健壯。當程序運行出現意外情形時,系統會自動生成一個Exception對象來通知程序,從而實現將"業務功能實現代碼"和“錯誤處理代碼”分離,提供更好的可讀性。
在這裏插入圖片描述


三、異常分類

在這裏插入圖片描述
從圖中可以看出,java把所有的非正常情況分成兩種:異常(Exception)和錯誤(Error),它們都繼承自Throwable父類。

1.Error

Error錯誤,一般是指與虛擬機相關的問題,如系統崩潰、虛擬機錯誤、動態鏈接失敗等,這種錯誤無法恢復或不可能捕獲 ,將導致應用程序中斷,通常應用程序無法處理也無法處理這些錯誤。

2.Exception

Exception異常,其它因編程錯誤或偶然的外在因素導致的一般性問題,如:空指針訪問、試圖讀取不存在的文件、網絡連接中斷。

這種異常是可以處理的,並且Exception又分爲了Checked Exception(受檢異常)RuntimeException(運行時異常)

  • Checked Exception(受檢異常)

受檢異常(編譯器要求必須處置的異常):正確的程序在運行中,很容易出現的、情理可容的異常狀況。可查異常雖然是異常狀況,但在一定程度上它的發生是可以預計的,而且一旦發生這種異常狀況,就必須採取某種方式進行處理。 比如說讀取數據庫時突然斷電,讀取文件時權限不夠等等。

比如:IOException/SQLException,這種異常不是程序本身的錯誤,只是外部因素如文件讀寫異常(讀寫權限不夠,文件不存在等等)、數據庫異常,但爲了保障程序的健壯性,必須要捕獲。

  • RuntimeException(運行時異常)

又叫非受檢異常,RuntimeException及其子類都是非受檢異常。比如空指針異常,除數爲0的算數異常ArithmeticException等等。這種異常程序可以不顯示處理,而是拋給JVM。

總結: checked異常表示檢查類的,這類異常發生是無法通過程序代碼可以避免發生,所以JAVA機制規定必須處理捕獲,而runtime異常是可以通過程序代碼可以避免發生,因此並不需要強制異常處理


四、異常的處理概述

java的異常處理是通過5個關鍵字來實現的:trycatchfinallythrowthrows
在這裏插入圖片描述


五、try-catch代碼塊

異常處理語法結構中,只有try塊是必需的,也就是說,如果沒有try塊,則不能有後面的catch塊和finally塊:catch塊和finally塊都是可選的,但catch塊和finally塊至少出現其中之一,也可以同時出現;可以有多個catch塊,捕獲父類異常的catch塊必須位於捕獲子類異常的後面;但不能只有try塊,既沒有catch塊,也沒有finally塊;多個catch塊必須位於try塊之後,finally塊必須位於所有的catch塊之後。

1.使用try-catch代碼塊捕獲異常分爲三種情況:

case 1: try中代碼塊沒有發生異常

在這裏插入圖片描述

case 2: try中代碼塊發生異常,並且與catch後的異常類型相匹配,則執行catch中的代碼

在這裏插入圖片描述

case 3: try中代碼塊發生異常,但是沒有與catch後的異常類型相匹配,則程序中斷

在這裏插入圖片描述

2.catch可以進行多異常捕獲

從Java7開始,一個catch塊可以捕獲多種類型的異常。

使用一個catch塊捕獲多種類型異常時需要注意:

  • 捕獲多種類型的異常時,多種異常類型之間用豎線(|)隔開
  • 捕獲多種類型的異常時,異常變量有隱式的final修飾,因此程序不能對異常變量重新賦值
catch(IndexOutOfBoundsException | ArithmeticException e){
    ...
}
3.在catch塊中處理異常(一般方法)

加入用戶自定義處理信息

System.out.println("出現錯誤:除數不能是0")

調用異常對象的方法輸出異常信息

e.printStackTrace();
方法名 說明
void printStackTrace( ) 輸出異常的堆棧信息
String getMessage( ) 返回異常信息描述的字符串是printStackTrace( )輸出信息的一部分

printStackTrace( )會輸出以下信息:

  • 異常發生的線程名
  • 發生的異常類型
  • 描述異常的信息
  • 異常發生的位置
4.finally代碼塊(不是必須)
  • 無論try中代碼是否有異常,該代碼都會執行(甚至try或catch執行了return語句)

  • 可以與try單獨使用

  • 適合:

    • 釋放資源

    • 關閉數據庫連接

//try-catch-finally舉例
public class DivTest{
	public static void main(String args){
     	try{
        	int a = Integer.parseInt(args[0]);
            int b = Integer.parseInt(args[1]);
            int c = a/b;
            System.out.println("您輸入的兩個數相除的結果是:" + c);
        }catch(IndexOutOfBoundsException ie){
			System.out.println("數組越界:輸入的參數不夠");
        }catch(NumberFormatException ne){
			System.out.println("數字格式異常:程序只能接受整數參數");
        }catch(ArithmeticException ae){
			System.out.println("算術異常:被除數不能是0");
        }catch(Exception e){
			System.out.println("未知異常");
        }finally{
			System.out.println("無論異常發生語法,這裏都會輸出");
        }
    }
}

catch異常的範圍遵循:先處理小異常,再處理大異常

如果我們把Exception類對應的catch塊排在其他catch塊的前面,Java運行時將直接進入該catch塊(因爲所有的異常對象都是Exception或其子類的實例),而排在它後面的catch塊將永遠也不會獲得執行的機會。

實際上,進行異常捕獲時不僅應該把Exception類對應的catch塊放在最後,而且所有父類異常的catch塊都應該排在子類異常catch塊的後面,否則將出現編譯錯誤。


六、受檢異常與非受檢異常的處理

Java認爲Checked異常都是可以被處理(修復)的異常,所以Java程序必須顯式處理Checked異常。如果程序沒有處理Checked異常,該程序在編譯時就會發生錯誤,無法通過編譯。

對於Checked異常的處理方式有如下兩種:

  • 當前方法明確知道如何處理該異常,程序應該使用try…catch塊來捕獲該異常,然後在對應的catch塊中修復該異常。

  • 當前方法不知道如何處理這種異常,應該在定義該方法時聲明拋出該異常

而Runtime異常則更加靈活,Runtime異常無須顯式聲明拋出,如果程序需要捕獲Runtime異常,也可以使用try…catch塊來實現。

1.使用throws拋出異常

使用throws聲明拋出異常的思路是,當前方法不知道如何處理這種類型的異常,該異常應該由上一級調用者處理:如果mian方法也不知道如何處理這種類型的異常,也可以使用throws聲明拋出的異常,這異常就會交給JVM處理。JVM對異常的處理辦法是,打印異常的跟蹤棧信息,並中止程序運行,這就是前面程序在遇到異常後自動結束的原因。

如果某段代碼中調用 了一個帶throws聲明的方法,該方法聲明拋出了Checked異常,則表明該方法希望它的調用者來處理該異常。也就是說,調用該方法時要麼放在try塊中顯式捕獲該異常,要麼放在另一個帶throws聲明拋出的方法中。

public class ThrowTest{
	public static void main(String[] args) throws Exception{
		//因爲test()方法聲明拋出IOException異常
        //所以調用該方法的代碼要麼處於try...catch塊中
        //要麼繼續使用throws向上拋異常
        test();
    }
    
    public static void test() throws Exception{
     	//因爲FileInputStream的構造器聲明拋出IOException異常
        //所以調用FileInputStream的代碼要麼處於try...catch塊中
		//要麼處於另一個帶throws聲明拋出的方法中
        FileInputStream fis = new FileInputStream("a.txt");
    }
}

使用throws聲明拋出異常時有一個限制,就是在方法重寫時:子類方法重寫時聲明拋出的異常不能比父類方法聲明的異常數量多或範圍廣

public class OverrideThrows{
    public void test() throws IOException{
        FileInputStream fis = new FileInputStream("a.txt");
    }
}

class SubClass extends OverrideThrows{
    //子類方法聲明拋出了比父類方法更大的異常
    //所以下面方法出錯
    public void test() throws Exception{
      
    }
}

由此可見,使用Checked異常至少存在如下兩大不便之處:

  • 對於程序中的受檢異常,Java要求必須顯式捕獲並處理該異常,或者顯式聲明拋出該異常。這樣就增加了編程的複雜度
  • 如果在方法中顯式聲明拋出受檢異常,將會導致方法調用與異常耦合,如果該方法是重寫父類的方法,則該方法拋出的異常還會受到被重寫方法所拋出異常的限制

在大部分情況下,推薦是用Runtime異常,而不使用受檢異常。尤其當程序需要自行拋出異常時,使用Runtime異常將更加簡潔。

2.使用throw拋出異常

當程序出現錯誤是,系統會自動拋出異常;除此之外,Java也允許程序自行拋出異常,自行拋出異常使用throw語句來完成。

很多時候,系統是否要拋出異常,可能需要根據應用的業務需求來決定,如果程序中的數據、執行與既定的業務需求不符,這就是一種異常。由於與業務需求不符而產生的異常,必須有程序員來決定是否拋出。

如果需要在程序中自行拋出異常,則應使用throw語句,throw語句可以單獨使用,throw語句拋出的不是異常類,而是一個異常實例,而且每次只能拋出一個異常實例。

public class ThrowTest{
	public static void main(String[] args){
        try{
            //調用聲明拋出Checked異常的方法,要麼顯式捕獲該異常
            //要麼在main方法中再次聲明拋出
            throwChecked(-3);
        }catch(Exception e){
			System.out.println(e.getMessage());	
        }
        
        //調用聲明拋出Runtime異常的方法既可以顯式捕獲該異常
        //也可不理會該異常
        throwRuntime(3);
    }
    
    public static void throwChecked(int a) throws Exception{
     	if(a > 0){
            //自行拋出Exception異常
            //該代碼必須處於try塊裏,或處於帶throws聲明的方法中
            throw new Exception("a的值大於0,不符合要求,主動拋出異常")
        }   
    }
    
    public static void throwRuntime(int a){
        if(a > 0){
         	//自行拋出RuntimeException異常,既可以顯式捕獲該異常
            //也可以完全不理會該異常,把該異常交給該方法調用者處理
            throw new RuntiomeException("a的值大於0,不符合要求");
        }
    }
}

通過上面程序也可以看出,自行拋出Runtime異常比自行拋出Checked異常的靈活性更好。同樣拋出Checked異常則可以讓編譯器提醒程序員必須處理該異常。

總結:

  • throw用於在方法中拋出某個特定異常
  • throws用於在方法聲明時,表示該方法可能拋出異常(一般是受檢異常),調用者需要處理

七、帶資源的try塊

FileputStream fis = null;

try{
    fis = new FileputStream("a.txt");
}
...
finally{
 	//關閉磁盤文件,回收資源
    if(fis != null){
        fis.close();
    }
}

從這個程序可以看出,當程序使用finally塊關閉資源時,程序顯得異常臃腫。Java7之後,增強了try語句的功能–它允許在try關鍵字後緊跟一堆圓括號,圓括號可以聲明、初始化一個或多個資源,此處的資源指的是那些必須在程序結束時顯示關閉的資源(比如數據庫連接,網絡連接等),try語句在該語句結束時自動關閉這些資源。

需要指出的是,爲了保證try語句可以正常關閉資源,這些資源實現類必須實現AutoCloseable或Closeable接口,實現這兩個接口就必須實現close()方法。

public static void main(String[] args){
	try(
        //聲明,初始化兩個可關閉的資源
        //try語句會自動關閉這兩個資源
        BufferedReader br = new BufferedReader(
        	new FileReader("AutoCloseTest.java"));
        PrintStream ps = new PrintStream(new 
            FileOutputStream("Demo.txt")))
        ){
		//使用兩個資源
        System.out.println(br.readLine());
        ps.println("hello world")}catch(Exception e){
		e.printStackTrace();
    }
}

自動關閉資源的try語句相當於包含了隱式的finally塊(這個finally塊用於關閉資源),因此這個try語句可以既沒有catch塊也沒有finally塊;也可以帶catch塊和一個finally塊

注: Java7幾乎把所有的“資源類”(包括文件IO的各種類,JDBC編程的Connection、Statement等接口…)進行了改寫,改寫後資源類都實現了AutoCloseable或Closeable接口


八、自定義異常

1.爲何需要自定義異常

當前異常無法滿足問題的描述,比如,程序規定當num<0時即爲異常,這時候就需要自定義異常。

2.Throwable中的常用方法
方法 作用
Throwable getCause() 返回異常的原因,沒有設置的話,返回null
String getMessage() 返回異常的詳細信息
Throwable initCause(Throwable cause) 設置異常的原因,與構造方法類似
void printStackTrace() 在標準輸出設備上輸出異常信息
3.如何自定義異常

用戶自定義異常都應該繼承Exception父類,如果希望自定義Runtime異常,則應該繼承RuntimeException父類。定義異常類時通常需要提供兩個構造器: 一個是無參數的構造器;另一個是帶一個字符串參數的構造器,這個字符串將作爲該異常對象的描述信息(也就是異常對象的getMessage()方法的返回值)

public class AuctionException extends Exception{
    //無參數的構造器
    public AuctionException(){ }
    //帶一個字符串參數的構造器
    public AuctionException(String msg){
        super(msg);
    }
}

super調用可以將此字符參數傳給異常對象的message屬性,該message屬性就是該異常對象的詳細描述信息。

如果需要自定義Runtime異常,只需將AuctionException.java程序中的Exception父類改爲RuntimeException父類,其他地方無須修改。

注:大部分情況下,創建自定義異常都可以採用與此代碼相似的代碼完成,只需改變類名使其可以準確描述該異常即可


九、重新拋出異常

重新拋出異常的使用場景:

  • 在向上傳播之前,先做一些處理
  • 對客戶端隱藏原始異常信息
  • 分層結果中降低層之間的耦合度

十、異常鏈(以後再寫)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章