08、異常處理

長路發表於2021-02-20


前言

      去年四月份大一下半學期正式開始學習Java,一路從java基礎、資料庫、jdbc、javaweb、ssm以及Springboot,其中也學習了一段時間資料結構。

      在javaweb期間做了圖書商城專案、ssm階段做了許可權管理專案,springboot學了之後手癢去b站看視訊做了個個人部落格專案(已部署到伺服器,正在備案中)。期間也不斷進行做筆記,總結,但是越學到後面越感覺有點虛,覺得自己基礎還有欠缺。

      之後一段時間我會重新回顧java基礎、學習一些設計模式,學習多執行緒併發之類,以及接觸一些jvm的相關知識,越學到後面越會感覺到基礎的重要性,之後也會以部落格形式輸出學習的內容。

      現在整理的java知識基礎點是在之前學習尚矽谷java課程的筆記基礎之上加工彙總,部分圖片會引用尚矽谷或網路上搜集或自己畫,在重新回顧的過程中也在不斷進行查漏補缺,儘可能將之前困惑的點都解決,讓自己更上一層樓吧。

      部落格目錄索引部落格目錄索引(持續更新)



一、異常概述與異常體系結構

概述說明

異常分類:編譯時異常與執行時異常

  • 編譯時異常:原始碼.java檔案在編譯器編譯時發生的錯誤。
  • 執行時異常:執行位元組碼檔案時發生不正確情況,其稱為異常。

這裡要講的異常指的是執行時異常,其分為兩類異常事件

  • Error:Java虛擬機器無法解決的嚴重問題。例如:JVM系統內部錯誤、資源耗盡等嚴重情況。比如:StackOverflowError(棧溢位)和OOM(記憶體溢位)。
  • Exception:其他因程式設計錯誤或偶然的外在因素導致的一般性問題,可以使用針對性的程式碼進行處理,例如空指標訪問、試圖讀取不存在檔案、網路連線中斷、陣列角標越界問題。

Error表示不可控異常一般不編寫針對性的程式碼進行處理;而對於Exception這類異常,我們可以編寫針對性的程式碼進行處理。


Error的例項

看Error的兩個例項:棧溢位與記憶體溢位

package com.mjj.pro7;

public class Test1 {
    public static void main(String[] args) {
        //1.棧溢位,無限壓棧 報錯 java.lang.StackOverflowError
        //main(args);
        //2.堆溢位,建立佔用超過初始jvm使用的記憶體,java.lang.OutOfMemoryError:
        Integer[] arr = new Integer[1024*1024*1024];
    }
}

針對於這Error的情況,我們不對其進行鍼對性處理。



二、常見異常

異常體系結構

非受檢異常:例如RuntimeException在預設情況下會得到自動處理,可以捕獲RuntimeException異常,但對於自己的封裝RuntimeException的異常,一部分還是需要進行手動丟擲。

受檢異常:Java編譯器要求程式必須捕獲或宣告丟擲這種異常。



RuntimeException舉例

表示執行時異常,接下來進行例項舉例

NullPointerException(空指標)

import org.junit.Test;

public class ExceptionTest {

    //NullPointerException
    @Test
    public void test1(){
        //例1
//      int[] arr = null;
//      System.out.println(arr[3]);
        
        //例2
        String str = null;
        System.out.println(str.charAt(0));
    }
}


IndexOutOfBoundsException(下標越界)

//IndexOutOfBoundsException
    @Test
    public void test2(){
        //第一種 ArraryIndexOutOfBoundsException
//      int[] arr = new int[10];
//      System.out.println(arr[10]);
        
        //第二種 StringIndexOutOfBoundsException
        String arr = "123";
        System.out.println(arr.charAt(3));
    }


ClassCastException(型別轉換)

//ClassCastException 型別轉換問題
@Test
public void test3(){
    Object obj = new Date();
    String str = (String)obj;
}


NumberFormatException(數值轉換)

//NumberFormatException 數值型別轉換
@Test
public void test4(){
    String str = "123";  //是通過的
    String str1 = "abc";
    int num = Integer.parseInt(str1);
}


InputMismatchException(輸入不匹配)

//InputMismatchException 輸入不匹配
@Test
public void test5(){
    Scanner sca = new Scanner(System.in);
    int num = sca.nextInt();  //當輸入abc時會報這個錯誤
    sca.close();
}


ArithmeticException(算術異常)

//ArithmeticException 算術異常
@Test
public void test6(){
    System.out.println(5/0);//java.lang.ArithmeticException: / by zero
}


三、異常處理概述

異常處理好處

:對於上面異常體系結構中不受檢異常指的是我們不進行異常處理系統也會自行捕捉到異常,並且輸出異常資訊。那麼我們處理異常與不處理異常的區別在哪以及為什麼要進行異常處理?

  • 區別描述:對不受檢異常不進行異常處理時,若我們程式發生異常,就會直接終止程式;若是進行異常處理,程式會按照我們要求進行異常處理並且繼續執行程式。
  • 目的:能夠讓我們對異常更好的處理以及程式繼續執行下去。

看一下是否進行異常處理的區別

①不進行異常處理

public class Main {
    public static void main(String[] args){
        String str = "abc";
        int number;
        //有異常的語句
        int i = Integer.parseInt(str);
        System.out.println(123);
    }
}

image-20210128195644491

可以看到一旦出現異常程式直接停止,後面的語句不再執行!!!

②進行異常處理

public class Main {
    public static void main(String[] args){
        String str = "abc";
        int number;
        //進行異常處理
        try {
            int i = Integer.parseInt(str);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        System.out.println(123);
    }
}

image-20210128195826541

我們可以看到程式完整的執行了下來後面的語句也進行了執行,這裡我們是對異常情況進行列印輸出!!!



抓拋模型

對異常的處理會有兩個過程:拋、抓

  • 過程一:"拋",程式在正常執行的過程中,一旦出現異常,就會在異常程式碼處生成一個對應異常類的物件,並將此物件丟擲。一旦丟擲物件以後,其後的程式碼都不執行。
  • 過程二:"抓",可以理解為對異常的處理方式,如:try-catch-finallythrows


異常處理機制一:try-catch-finally

語法

try{
//可能出現異常的程式碼
}catch(異常型別1 變數名1){
//處理異常的方式1
}catch(異常型別2 變數名2){
//處理異常的方式2
}
...
finally{
//一定會執行的程式碼
}

try-catch注意點

  1. try中包裹可能出現異常的程式碼,若出現異常先生成指定異常,接著去catch中去找匹配異常。
  2. 當try中出現錯誤,進入catch部分中進行異常處理,一旦處理完就執行finally中包裹內容(finally存在),接著跳出try-catch結構,繼續執行try-catch結構之外的程式碼。
  3. catch中的異常型別如果沒有子父類關係,誰在上誰在下都無所謂;若是多個catch中有子父類關係的,子類必須要宣告在父類異常之上,否則報錯。父類在上的話,子類異常宣告在下就沒意義了!!!
  4. catch的{}中異常物件處理方式常見的兩種:
    • e.getMessage():獲取異常的簡略資訊。
    • e.printStackTrace():比較常用,列印完整的堆疊情況,會顯示指定的出錯位置。
  5. 注意在try中宣告的變數(區域性變數),其生命週期只在try結構中,try-catch結構之外無法呼叫。

注意點5中的例項演示:

public class Main {
    public static void main(String[] args){
        String str = "abc";
        int number;
        try {
            int i = Integer.parseInt(str);
        } catch (NumberFormatException e) {
            //e.printStackTrace(); //詳細堆疊錯誤資訊
            System.out.println(e.getMessage());//簡略資訊
        }
        
    }
}

image-20210128194507363

image-20210128194559185



finally注意點

  1. finally中宣告的程式碼是一定執行的,儘管catch中出現異常會先執行finally,再丟擲異常。
  2. try-catch-finally使用於方法中catch與finally包含return,無論catch是否有異常,都會執行finally中的return返回

注意點1中情況舉例

public class Main {
    public static void main(String[] args){
        try{
            int a=10;
            int b=0;
            System.out.println(a/b);
        }catch(ArithmeticException e){
            int[] a = new int[10];
            //這裡有陣列越界異常
            System.out.println(a[10]);
        }
      finally{
          System.out.println("執行finally語句");
      }
        System.out.println("長路哥哥");
    }
}
  • catch中如果出現異常,只會執行finally中的程式碼,try-catch結構外的也不會處理!!!

image-20210128200901666


注意點2中舉例

public class Main {
    public static void main(String[] args){
        System.out.println(Main.method());
        System.out.println(123456);
    }

    public static int method(){
        try{
            System.out.println(10/0);
        }catch(ArithmeticException e){
            int[] a = new int[10];
            //這裡有陣列越界異常
            System.out.println(a[10]);
            return 2;
        }
        finally{
            return 3;
        }
    }
}

image-20210128202359796

可以看到結果沒有出現異常,說明返回的是finally中的。

原因:在方法中catch出現異常了,會直接先去執行finally中內容,這裡finally中是返回值,那麼當catch異常處理前方法已經結束了,所以沒有報異常出來!!!



finally實際使用

例如資料庫連線、輸入輸出流,網路程式設計Socket等資源,JVM是不能自動的回收的,我們需要自己手動的進行資源的釋放,此時的資源釋放,就需要宣告在finally中。

下面例子是演示輸入輸出流的關閉

import java.io.*;

public class Main {
    public static void main(String[] args){
        File file = new File("hello.txt");
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            int read = fis.read();
            while(read != -1){
                System.out.println((char)read);
                read = fis.read();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //fis放在這裡進行關閉資源 再使用一個try-catch是因為IOException是受檢型的必須宣告
            try {
                if(fis != null)
                    fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

在專案工程目錄下新增hello.txt檔案後執行

image-20210128212349704



總結

使用於main()中
try-catch:try異常,catch無異常進行處理,執行try-catch結構外的。
try-catch:try異常,catch異常,直接結束程式(列印異常)。
try-catch-finally:try異常,執行指定catch中,無異常會先執行完catch內容,接著執行finally中內容,然後執行try-catch以外內容。
try-catch-finally:try異常,執行指定catch中若是有異常會先執行finally內容,接著程式結束(列印異常)。

呼叫單獨方法中
try-catch:try異常,catch無異常,正常執行後序程式。
try-catch:try異常,catch異常,程式直接結束(列印異常)。
try-catch-finally:catch與finally中都有返回值,若是catch中出現異常,會先去找finally,之後直接結束方法,此時異常也不會列印。
無try-catch結構捕捉異常,也無throws丟擲,對於出現非受檢查的異常系統會自動丟擲。

總結總結:catch中無異常,執行完catch後執行finally以及try-catch結構之外的;catch中若是出現異常會先去執行finally中內容,接著程式直接停止。



異常處理機制二:throws

認識及使用throws

語法:throws 異常類

//例如
public void method()throws RuntimeException{
    
}

寫在方法的宣告處,指明此方法執行時可能會丟擲的異常。一旦方法體執行時,出現異常,仍會在異常程式碼處生成一個異常類的物件,若此物件滿足throws的異常就會丟擲,在方法中出現異常程式碼後續的程式碼就不會執行。

與try-catch-finally比較

  • try-catch-finally:真正將異常處理掉。
  • throws:將異常拋給方法的呼叫者,並沒有真正將異常處理掉。

例項1:throws宣告可能會丟擲的異常(表示了一種規範),方法中出現異常則會向上傳遞:

import java.io.*;

public class Main {
    public static void main(String[] args){
        try {
            method1();
        } catch (RuntimeException e) {
            System.out.println("捕捉到方法中的異常");
        }
    }

    public static void method1() throws RuntimeException {
        method2();
    }

    public static void method2 () throws RuntimeException{
        System.out.println(5/0);
    }
}

image-20210128223719226

其實就算我兩個方法都不新增throws RuntimeException,最終使用try-catch也是能夠接收到的,那為什麼要使用thorows宣告勒?也可以算是一種規範吧,宣告則指明其方法可能會出現的異常,好讓呼叫方法者知道捕捉異常時使用什麼型別捕捉!!!不宣告的話使用try-catch結構中catch預設會是Exception。



重寫方法異常丟擲規則

對於重寫方法throws的規則

  • 子類重寫方法的丟擲異常型別應當小於或等於父類方法丟擲異常型別
  • 父類中沒有throws異常,子類重寫時也不應該有

例項如下:

class SuperClass{

    public void method()throws IOException{

    }
}

class SubClass extends SuperClass{   //繼承SuperClass
    @Override
    public void method() throws FileNotFoundException {  //子類重寫方法異常應當小於等於父類的異常

    }
}


開發中如何選擇異常處理機制

  1. 若是被重寫父類的方法中沒有throws,那麼子類重寫的方法必定也沒有throws,若重寫方法有異常,必須使用try-catch解決。
  2. 若呼叫多個不同的方法,並且幾個方法是遞進關係,那麼建議使用throws,最先執行呼叫的方法使用try-catch捕捉異常。

try(){}語法

Java7 build 105版開始,Java7的編譯器和執行環境支援新的 try-with-resources 語句,稱為 ARM 塊(Automatic Resource Management) ,自動資源管理。

語法如:try塊退出時,會自動呼叫res.close()方法,關閉資源

try(Resource res = xxx)//可指定多個資源
{
     work with res
} 

這相比我們之前在finally中一個個手動關閉資源好的多。

我們看一下兩者對比

//以前try{}catch{}:
FileInputStream fis = null;
FileOutputStream fos = null;
try {
    //目標圖片1234.jpg
    fis = new FileInputStream(new File("1234.jpg"));
    //複製地址
    fos = new FileOutputStream(new File("圖片.jpg"));
    ....
} catch (IOException e) {
    e.printStackTrace();
}finally {
    if(fis != null){
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    if(fos != null){
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

//現在的try(){}
try (
    FileInputStream fis = new FileInputStream(new File("1234.jpg"));
    FileOutputStream fos = new FileOutputStream(new File("圖片.jpg"));
	){
    ....
} catch (IOException e) {
    e.printStackTrace();
}

省去了大量的語句,舒服!!!



四、手動丟擲異常throw

介紹一下異常物件的產生:①系統在出現異常時自動生成異常物件②手動生成一個異常物件,例如throw new Exception()

只要是使用了throw,就一定表示丟擲了一個異常,而throws只是用於宣告可能丟擲的異常便於呼叫其方法的人做出相應的異常處理!!

例項:手動丟擲異常,並使用throws宣告該方法可能會有什麼異常

public class Main {
    public static void main(String[] args){
        try {
            Main.method("");
        } catch (RuntimeException e) {
            System.out.println(e.getMessage());
        }
    }

    public static void method(String str)throws RuntimeException{
        if(!"".equals(str)){
            System.out.println(str);
        }else {
            throw new RuntimeException("str不能為空");
        }
    }

}

image-20210128233116965



五、自定義異常類

首先問一下為什麼要自定義異常類勒?有幾點原因,例如設定多個自己定義的異常類,僅僅捕獲其自己所關心的異常類,並知道如何處理;根據自己的需求出現異常的時候來去做特殊的處理;異常分類,業務中不同場景丟擲不同異常,便於統一捕獲並根據型別做進一步處理

對於自定義異常類有幾點規範如下

  • 繼承於現有的異常類如:Exception、RuntimeException....
  • 自定義異常類需要提供一個全域性常量如:serialVersionUID ,用來標識自己定義的異常類
  • 必須提供過載的構造器

自定義異常:包含最基本的幾個部分

class MyException extends RuntimeException{
    //需要一個UID來表示自己的自定義異常類
    static final long serialVersionUID = -7034897190745766959L;

    public MyException(){
    }

    //想要有自定義異常描述,就需要有一個有參構造
    public MyException(String msg){
        super(msg);
    }
}
//測試上面的自定義異常
public class Main {
    public static void main(String[] args){
        try {
            Main.method("");
        } catch (MyException e) {
            System.out.println(e.getMessage());
        }
    }

	//測試使用
    public static void method(String str)throws MyException{
        if(!"".equals(str)){
            System.out.println(str);
        }else {
            throw new MyException("自定義異常描述:str不準為空");
        }
    }

}

image-20210128235408009



參考資料

[1]. Java受檢異常和非受檢異常

[2]. Java基礎之《受檢查異常和不受檢查異常》

[3]. Java 中的異常和處理詳解

[4]. Java中關鍵字throw和throws的區別

[5]. Java:為什麼要有自定義異常?

[6]. Java自定義異常(優雅的處理異常) 實際應用配合列舉

[7]. java try(){}catch(){}自動資源釋放



我是長路,感謝你的閱讀,如有問題請指出,我會聽取建議並進行修正。
歡迎關注我的公眾號:長路Java,其中會包含軟體安裝等其他一些資料,包含一些視訊教程以及學習路徑分享。
學習討論qq群:891507813 我們可以一起探討學習
註明:轉載可,需要附帶上文章連結