1. 程式人生 > 實用技巧 >跟光磊學Java開發-Java異常機制

跟光磊學Java開發-Java異常機制

跟光磊學Java開發-Java異常機制

跟光磊學Java開發

異常概述

什麼是異常

異常指的時程式在執行的過程中出現的不正常情況導致JVM終止程式執行。

 /**
     * java.lang.ArithmeticException: / by zero
     */
    @Test
    public void testArithmeticException(){
        System.out.println("開始執行");
        //這裡會丟擲ArithmeticException,即除數不能為0
        System.out.println(1
/0); //JVM遇到異常不會再往下執行 System.out.println("結束執行"); }

程式執行結果

Java是面向物件的程式語言,而異常在Java中本身也是一個類,當出現異常的時候,JVM就會建立該異常的物件並丟擲該異常物件。
例如當執行System.out.println(1/0);時JVM會建立一個java.lang.ArithmeticException異常物件,該物件包含了異常的型別、異常的資訊、異常的位置。

Java異常體系結構


Java異常體系結構

  • java.lang.Throwable 是Java語言所有的錯誤和異常的頂級父類
  • java.lang.Error是Java語言所有錯誤的頂級父類,這裡的錯誤是指的程式執行時的錯誤,而不是編譯時的語法錯誤。Error無法通過程式碼修正,只能事先避免。類似於人得了絕症
    例如棧記憶體溢位錯誤,堆記憶體溢位錯誤,伺服器宕機等等。

這裡使用方法的遞迴造一個棧溢位錯誤

    /**
     * 棧溢位錯誤
     */
    @Test
    public void testStackOverFlowError(){
        createStackOverFlowError();
    }

    /**
     * 使用方法遞迴製造一個棧溢位異常
     */
    public
void createStackOverFlowError()
{ System.out.println("I'm StackOverFlowError"); createStackOverFlowError(); }

程式執行結果

  • java.lang.Exception時Java語言所有異常的頂級父類,可以通過程式碼進行修正,類似於人得了小感冒。
    Java的異常分為編譯時異常和執行時異常

編譯異常就是在程式編譯期間出現的異常導致程式無法編譯通過編譯。不是RuntimeException或者其子類就時編譯異常。

之前使用SimpleDateFormat物件的parse()方法將字串轉換為Date日期就是一個編譯時異常

 /**
     * 編譯時異常
     */
    @Test
    public void testCompileTimeException(){
        SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd");
        Date date=format.parse("2020-12-21");
    }


編譯異常

而執行時異常時程式執行時發生的異常,例如之前的ArithmeticException,執行時異常都是java.lang.RuntimeException的子類,反之其他的類都是編譯時異常。

異常的處理流程

以陣列索引越界的執行時異常為例說明異常是如何產生的和處理的


/**
     * 以陣列越界異常的執行時異常為例,說明異常的產生過程
     */
    @Test
    public void testArrayIndexOutOfBoundsException(){
        int[]data={1,2,3};
        int element=getElement(data,3);
        System.out.println(element);
    }


    public Integer getElement(int[]data,int index){
        if(data!=null&&data.length>0&&index>=0){
            return data[index];
        }
        return null;
    }

程式執行結果

程式執行時testArrayIndexOutOfBoundsException()方法執行,當該方法呼叫getElement()方法時將data陣列和索引3傳給getElement()方法,由於data陣列沒有索引為4的元素,所以產生JVM能夠識別的java.lang.ArrayIndexOutOfBoundException異常,JVM會建立ArrayIndexOutOfBoundException類的物件,也就是new ArrayIndexOutOfBoundsException(3),建立異常物件後封裝異常資訊,包含異常的型別、異常的資訊、異常的位置資訊。異常物件建立完畢後JVM使用 throw new ArrayIndexOutOfBoundException(message)將該異常物件拋給main()方法,main()方法接收異常物件後由於main方法沒有處理該異常的程式碼,所以main()方法會繼續把異常資訊轉遞給呼叫者JVM,JVM接收到該異常物件後首先把異常物件資訊列印到控制檯,然後終止程式執行。

異常的產生與處理

產生異常

ArrayIndexOutOfBoundException是JVM自動產生的異常
程式中如何手動產生一個異常呢?
首先建立一個異常類的物件,然後使用throw丟擲即可。
throw用於方法內,用來丟擲一個指定的異常物件,將這個異常物件傳遞給呼叫者,並結束當前方法的執行。
throw關鍵字的使用格式

throw new 異常類名(引數);

例如throw new RuntimeException("年齡不合法");此處的 new RuntimeException("年齡不合法");就是一個RuntimeException的匿名物件

在自定義的類Employee類中有個私有化成員變數age,提供了get/set方法,我們可以在setAge(int age)方法當年齡不合法時丟擲一個RuntimeException

  public void setAge(Integer age) {
        if(age<0||age<120){
            //產生一個RuntimeException並拋給方法的呼叫方,然後結束方法
            throw new RuntimeException("年齡不合法");
        }
        else{
            this.age = age;
        }
    }

然後在呼叫setAge()方法時傳入一個不合法的年齡

  @Test
    public void testCreateException(){


        Employee employee=new Employee();
        //如果年齡不符合就產生RuntimeException
        employee.setAge(-12);
        employee.setEmployeeNo("ittimeline000001");
        //這裡不會再執行,因為沒有處理異常
        System.out.println(employee);
    }

程式執行結果

當呼叫到setAge()方法時由於傳遞了一個不合法的年齡,因此在setAge()方法內部丟擲了一個RuntimeException給呼叫者,也就是ExceptionTest類中的testCreateException()方法,由於該方法並沒有處理RuntimeException,因此虛擬機器直接將RuntimeException的異常資訊直接列印在終端後終止程式的執行。

異常的處理

異常的處理有兩種方式,一種是宣告處理異常,另外一種是捕獲處理異常。

  1. 宣告處理異常
    宣告處理是使用throws關鍵字將異常標識出來,表示當前方法不處理異常,而是提醒呼叫者,讓呼叫者來處理。如果呼叫者不處理最終會到JVM,JVM會直接列印異常資訊,結束程式。

定義throwSingleException()方法,該方宣告一個ParseException,用於解決該方法內部throw new ParseException("解析異常",5);丟擲的 ParseException,由於ParseException屬於編譯時異常,因此在執行前就需要處理。

   /**
     * 丟擲單個編譯時異常
     * 編譯時異常必須在執行前處理
     * 這裡使用throws宣告丟擲
     * @throws ParseException 解析異常
     */
    public void throwSingleException()throws ParseException{
        throw  new ParseException("解析異常",5);
    }

在testThrowsSingleExceptionHandleException()方法中繼續使用throws丟擲ParseException

    /**
     * 宣告處理異常
     * 當程式有異常時會造成程式異常終止,導致程式無法繼續往下執行
     */
    @Test
    public void testThrowsSingleExceptionHandleException()throws ParseException{
        System.out.println("開始執行");
        throwSingleException();
        System.out.println("結束執行");
    }

程式執行結果

此時JVM會將該異常資訊列印在終端,並終止程式,由於程式停止所以字串結束程式並沒有列印在終端上。

宣告處理異常時可以宣告同時宣告多個異常,多個異常之間使用逗號分隔
而throwMultiException()方法內部丟擲了兩個編譯時異常,因此使用throws IOException,ParseException宣告處理異常的方式來處理異常

    /**
     * 丟擲多個編譯時異常
     * 編譯時異常必須在執行前處理
     * 這裡使用throws丟擲多個異常,多個異常之間使用逗號分隔
     * @param number
     * @throws IOException
     * @throws ParseException
     */
    public void throwMultiException(int number) throws IOException,ParseException{
        if(number==1){
            throw  new IOException("IO異常");
        }
        else{
            throw new ParseException("解析異常",5);
        }
    }

在方法testThrowsMultiExceptionHandleException()呼叫時繼續使用throws ParseException,IOException拋給JVM

  /**
     * 宣告處理異常
     * 當程式有異常時會造成程式異常終止,導致程式無法繼續往下執行
     */
    @Test
    public void testThrowsMultiExceptionHandleException()throws ParseException,IOException{
        System.out.println("開始執行");
        throwMultiException(1);
        throwMultiException(2);
        System.out.println("結束執行");
    }

程式執行結果

JVM處理IOException時直接列印異常資訊並終止程式的執行,因此字串結束執行並沒有列印在終端上。

  1. 捕獲處理異常
    捕獲處理異常是對異常進行捕獲處理,捕獲處理異常後程序可以繼續向下執行。
    java使用try/catch 來捕獲處理異常
try{
		可能會出現的異常的程式碼
}catch(異常型別 變數名){
		處理異常的程式碼
}

如果try中的程式碼塊出現了異常,就會執行catch裡面的程式碼,程式會繼續往下執行。
如果try中的程式碼沒有異常,就不會執行catch裡面的程式碼,程式會繼續往下執行。
使用try/catch時注意try和catch是成對出現的,而try中的某個位置的程式碼出現異常, 在某個位置後的程式碼不會再執行(例如這裡的end try並沒有輸出在終端),轉而執行catch中的程式碼,catch的程式碼執行後程序繼續執行剩下的程式碼。
java.lang.Throwable類定義了一些檢視異常的方法

  • public String getMessage():獲取異常的描述資訊,也就是異常原因,通常提示給使用者。
  • public String toStringg():獲取異常的型別資訊和異常描述資訊
  • public void printStackTrace()列印異常的跟蹤資訊並輸出到控制檯
  /** 捕獲異常 */
  @Test
  public void testTryCatchException() {
    System.out.println("開始執行");

    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
    try {
      System.out.println("begin try");
      // 可能會發生異常
      Date date = format.parse("2020年12月21日");
      System.out.println("end try");

    } catch (ParseException e) {
      System.out.println("begin catch");
      // 處理異常
      // 列印異常堆疊到控制檯
      e.printStackTrace();
      System.out.println("end catch");

    }
    System.out.println("結束執行");

  }

程式執行結果

在捕獲處理異常時,異常可能會引發程式跳轉,導致有些語句執行不到,而finally就是解決這個問題,即如果有一些特定的程式碼無論異常是否發生,但是都需要執行時便可以使用finally。
,如果沒有呼叫System.exit()退出虛擬機器或者系統宕機的等極端情況下在finally塊的程式碼一定會被執行。

finally塊通常都是和try/catch一起使用

try{
  		//可能會發生異常的程式碼
 
}catch(異常型別 變數){
	//處理異常
}finally{
	//此處的程式碼一定會執行
}

try/catch/finally的執行流程

  1. 首先執行try程式碼塊
  2. 如果try程式碼塊沒有異常那麼就不會執行catch的程式碼塊,直接執行finally程式碼塊,然後繼續執行finally程式碼塊之後的程式碼
  3. 如果try程式碼塊有異常,那麼就執行catch的程式碼塊,catch程式碼塊執行完成後就繼續執行finally程式碼塊,然後繼續執行finally程式碼塊之後的程式碼

使用try/catch/finally捕獲異常, try中沒有異常也會執行finally, 除非呼叫了System.exit(0)退出虛擬機器,finally程式碼塊不會執行

 /**
   * 使用try/catch/finally捕獲異常
   * try中沒有異常也會執行finally
   * 除非呼叫了System.exit(0)退出虛擬機器,finally程式碼塊不會執行
   *
   */
  @Test
  public void testTryCatchFinallyWithoutException() {
    Scanner input = null;
    try {
      input = new Scanner(System.in);
      System.out.println("1 / 1 = "+(1 / 1));

    } catch (Exception e) {
      System.out.println("程式出現了異常");
      e.printStackTrace();
      //結束方法
      return ;
    } finally {
      // 無論是否發生異常,都會執行,除非呼叫了System.exit(0)退出虛擬機器
      input.close();
      System.out.println("finally 關閉Scanner流");

    }
  }


程式執行異常

使用try/catch/finally捕獲異常,即使catch中有return, finally程式碼塊依然會執行

 /**
   * 使用try/catch/finally捕獲異常
   * 即使catch中有return
   * finally程式碼塊依然會執行
   */
  @Test
  public void testTryCatchFinallyWithException() {
    Scanner input = null;
    try {
      input = new Scanner(System.in);
      System.out.println(1 / 0);
      input.close();
      System.out.println("try 關閉Scanner流");

    } catch (Exception e) {
      System.out.println("程式出現了異常");
      e.printStackTrace();
      //結束方法
      return ;
    } finally {
      // 無論是否發生異常,都會執行
      input.close();
      System.out.println("finally 關閉Scanner流");

    }
  }

異常的注意事項

  1. 執行時異常可以不處理,既不捕獲處理異常也不宣告處理異常。但是JVM會將異常資訊列印在終端,然後終止程式,異常之後的程式碼沒有機會執行,此時可以使用try/catch捕獲異常
  2. 如果父類的方法throws宣告丟擲了多個異常。子類重寫父類的方法時,只能throws宣告丟擲相同的異常或者它的子集
/**
   * 子類重寫父類方法的注意事項
   */
  @Test
  public void testMethodOverrideThrowsException(){
    
    Father father=new Child();
    try {
      father.getFileInfo();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }



  class Father{
    /**
     * 獲取檔案資訊
     */
    public void getFileInfo()throws IOException{
      
    }
  }
  
  class Child extends Father{
    /**
     * 重寫父類的方法時 throws 異常只能是IOException或者它的子類
     * @throws FileNotFoundException
     */
    @Override
    public void getFileInfo() throws FileNotFoundException {
    }
  }
  1. 父類方法沒有丟擲異常,子類覆蓋父類方法時也不可以丟擲異常,此時子類產生異常,只能夠try/catch捕獲處理,不能throws宣告丟擲
  class Father{
	/**
		 * 獲取檔案資訊
		 */
    public void getFileInfo(){

    }
  }

  class Child extends Father{
    /**
     * 重寫父類的方法時 子類有異常不能throws丟擲,只能自己try/catch處理
     * @throws FileNotFoundException
     */
    @Override
    public void getFileInfo()  {
      try {
        throw new FileNotFoundException();
      } catch (FileNotFoundException e) {
        e.printStackTrace();
      }
    }
  }
  1. 當try/catch後追加finally程式碼塊,如果之前沒有呼叫System.exit(0)退出JVM等極端情況,finally程式碼塊一定會執行,通常用於資源釋放
  2. 捕獲處理如何處理多個異常?
  • 多個異常分別處理
 /**
   * 捕獲多個異常
   * 多個異常分別處理
   */
  @Test
  public void testMultiTryMultiCatchException(){

    int number=1;
    //處理ArithmeticException
    try {
      if(number==1){
        throw  new ArithmeticException("數字非法");
      }
    } catch (ArithmeticException e) {
      e.printStackTrace();
    }
    number=2;
    //處理RuntimeException
    try {
      if(number==2){
        throw  new ParseException("數字2不能解析",2);
      }
    } catch (ParseException e) {
      e.printStackTrace();
    }
  }

程式執行結果

  • 多個異常一次捕獲,多次處理

  public void oneTryMultiCatch(int number) {
    try {

      if (number == 1) {
        throw new ArithmeticException("數字非法");
      }
      // 這裡不會再執行,因為已經丟擲了ArithmeticException異常
      number = 2;
      if (number == 2) {
        throw new ParseException("數字2不能解析", 2);
      }
      // JDK7以後支援catch中同時處理多個同級的異常
    } catch (ArithmeticException | ParseException e) {
      e.printStackTrace();
    }
  }

  /** 多個異常一次try多次catch */
  @Test
  public void testOneTryMultiCatchException() {

    int number = 1;
    oneTryMultiCatch(number);
    number = 2;
    oneTryMultiCatch(number);
  }


程式執行結果

  • 多個異常一次捕獲,一次處理
public void oneTryMultiOneCatch(int number) {
    try {

      if (number == 1) {
        throw new ArithmeticException("數字非法");
      }
      // 這裡不會再執行,因為已經丟擲了ArithmeticException異常
      number = 2;
      if (number == 2) {
        throw new ParseException("數字2不能解析", 2);
      }
      //同時處理多個異常
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /** 多個異常一次try一次catch */
  @Test
  public void testOneTryOneCatchException() {

    int number = 1;
    oneTryMultiOneCatch(number);
    number = 2;
    oneTryMultiOneCatch(number);
  }

程式 執行結果

需要注意的是,當多個異常分別處理時,捕獲處理,前邊的類不能是後邊的父類

/**
   * 異常處理的順序
   * 前面的catch中的Exception物件不能是後面異常物件的父類
   */
  @Test
 public void testExceptionOrder() {

    int number = 1;
    try {
      if (number == 1) {
        throw new ArithmeticException("數字不能是1");
      }
    } catch (Exception e) {
      e.printStackTrace();
      //    }catch (RuntimeException e){
      //      e.printStackTrace();
      //    }
    }
}


異常的順序

自定義異常

在進行業務開發時,通常需要結合自身的業務場景自定義異常,因為JDK沒有根據各自系統定義業務異常類。

自定義異常分為編譯時異常和執行時異常。

  • 編譯時異常繼承自java.lang.Exception
  • 執行時異常繼承自java.lang.RuntimeException

在網站註冊時通常會檢測使用者名稱是否已經存在,如果使用者名稱存在則不能註冊。
這裡編寫一個自定義異常,該異常是RuntimeException的子類,即在執行時才會發生的異常

package net.ittimeline.java.core.jdk.api.lang.bean.exception;

/**
 * 註冊異常
 *
 * @author tony [email protected]
 * @version 2020/12/21 13:02
 * @since JDK11
 */
public class RegisterException  extends RuntimeException{


    /**
     * 預設構造器
     */
    public RegisterException(){
        super();
    }

    /**
     * 帶異常資訊的構造器
     * @param message
     */
    public RegisterException(String message){
        super(message);
    }
}

然後在註冊程式中使用,判斷如果使用者名稱存在,就使用throw丟擲該異常,因為RegisterException是RuntimeException,所以不能在main方法throws宣告丟擲。
而使用了try/catch/finally來捕獲處理異常

package net.ittimeline.java.core.jdk.api.lang.bean;

import net.ittimeline.java.core.jdk.api.lang.bean.exception.RegisterException;

import java.util.Scanner;

/**
 * 自定義異常測試類
 *
 * @author tony [email protected]
 * @version 2020/12/21 13:04
 * @since JDK11
 */
public class RegisterExceptionTest {

  public static void main(String[] args) {
    // 已經存在的使用者名稱
    String targetUserName = "tony";
    Scanner input = new Scanner(System.in);
    System.out.println("請輸入你的使用者名稱");
    String userName = input.nextLine();
    try {
      if (userName.equals(targetUserName)) {
        //丟擲異常
        throw new RegisterException("使用者已經存在");
      } else {
        System.out.printf("恭喜%s註冊成功\n", userName);
      }
    } catch (RegisterException e) {
      //處理異常
      //列印異常的堆疊資訊
      e.printStackTrace();
    } finally {
      //關閉鍵盤的輸入流
      input.close();
    }
  }
}

程式執行結果