Java異常處理機制

  • 2019 年 10 月 3 日
  • 筆記

Java 中的異常(Exception)又稱為例外,是一個在程式執行期間發生的事件,它中斷正在執行的程式的正常指令流。為了能夠及時有效地處理程式中的運行錯誤,必須使用異常類。

一、異常簡介

在程式中,錯誤可能產生於程式設計師沒有預料到的各種情況,或者超出程式設計師可控範圍的環境,例如用戶的壞數據、試圖打開一個不存在的文件等。為了能夠及時有效地處理程式中的運行錯誤,Java 專門引入了異常類。

例 1

為了更好地理解什麼是異常,下面來看一段非常簡單的 Java 程式。下面的示例程式碼實現了允許用戶輸入 1~3 以內的整數,其他情況提示輸入錯誤。

package ch11;  import Java.util.Scanner;  public class TestO1  {      public static void main(String[] args)      {          System.out.println("請輸入您的選擇:(1~3 之間的整數)");          Scanner input=new Scanner(System.in);          int num=input.nextInt();          switch(num)          {              case 1:                  System.out.println("one");                  break;              case 2:                  System.out.println("two");                  break;              case 3:                  System.out.println("three");                  break;              default:                  System.out.println("error");                  break;          }      }  }

 

正常情況下,用戶會按照系統的提示輸入 1~3 之間的數字。但是,如果用戶沒有按要求進行輸入,例如輸入了一個字母“a”,則程式在運行時將會發生異常,運行結果如下所示。


請輸入您的選擇:(1~3 之間的整數)  a  Exception in thread "main" java.util.InputMismatchException  at java.util.Scanner.throwFor(Unknown Source)  at java.util.Scanner.next(Unknown Source)  at java.util.Scanner.nextInt(Unknown Source)  at java.util.Scanner.nextInt(Unknown Source)  at text.text.main(text.java:11)

 

二、異常產生的原因及使用原則

在 Java 中一個異常的產生,主要有如下三種原因:

  1. Java 內部錯誤發生異常,Java 虛擬機產生的異常。
  2. 編寫的程式程式碼中的錯誤所產生的異常,例如空指針異常、數組越界異常等。這種異常稱為未檢査的異常,一般需要在某些類中集中處理這些異常。
  3. 通過 throw 語句手動生成的異常,這種異常稱為檢査的異常,一般用來告知該方法的調用者一些必要的資訊。


Java 通過面向對象的方法來處理異常。在一個方法的運行過程中,如果發生了異常,則這個方法會產生代表該異常的一個對象,並把它交給運行時的系統,運行時系統尋找相應的程式碼來處理這一異常。

我們把生成異常對象,並把它提交給運行時系統的過程稱為拋出(throw)異常。運行時系統在方法的調用棧中查找,直到找到能夠處理該類型異常的對象,這一個過程稱為捕獲(catch)異常。

Java 異常強制用戶考慮程式的強健性和安全性。異常處理不應用來控制程式的正常流程,其主要作用是捕獲程式在運行時發生的異常並進行相應處理。編寫程式碼處理某個方法可能出現的異常,可遵循如下三個原則:

  1. 在當前方法聲明中使用 try catch 語句捕獲異常。
  2. 一個方法被覆蓋時,覆蓋它的方法必須拋出相同的異常或異常的子類。
  3. 如果父類拋出多個異常,則覆蓋方法必須拋出那些異常的一個子集,而不能拋出新異常。

 

三、異常類型

在 Java 中所有異常類型都是內置類 java.lang.Throwable 類的子類,即 Throwable 位於異常類層次結構的頂層。Throwable 類下有兩個異常分支 Exception 和 Error,如圖 1 所示。


圖1 異常結構圖


由圖 1 可以知道,Throwable 類是所有異常和錯誤的超類,下面有 Error 和 Exception 兩個子類分別表示錯誤和異常。其中異常類 Exception 又分為運行時異常和非運行時異常,這兩種異常有很大的區別,也稱為不檢查異常(Unchecked Exception)和檢查異常(Checked Exception)。

  • Exception 類用於用戶程式可能出現的異常情況,它也是用來創建自定義異常類型類的類。
  • Error 定義了在通常環境下不希望被程式捕獲的異常。Error 類型的異常用於 Java 運行時由系統顯示與運行時系統本身有關的錯誤。堆棧溢出是這種錯誤的一例。
  • 本章不討論關於 Error 類型的異常處理,因為它們通常是災難性的致命錯誤,不是程式可以控制的。本章接下來的內容將討論 Exception 類型的異常處理。


運行時異常都是 RuntimeException 類及其子類異常,如 NullPointerException、IndexOutOfBoundsException 等,這些異常是不檢查異常,程式中可以選擇捕獲處理,也可以不處理。這些異常一般由程式邏輯錯誤引起,程式應該從邏輯角度儘可能避免這類異常的發生。

非運行時異常是指 RuntimeException 以外的異常,類型上都屬於 Exception 類及其子類。從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如 IOException、ClassNotFoundException 等以及用戶自定義的 Exception 異常,一般情況下不自定義檢查異常。表 1 列出了一些常見的異常類型及它們的作用。

 

表1 Java中常見的異常類型
異常類型 說明
Exception 異常層次結構的根類
RuntimeException 運行時異常,多數 java.lang 異常的根類
ArithmeticException 算術譜誤異常,如以零做除數
ArraylndexOutOfBoundException 數組大小小於或大於實際的數組大小
NullPointerException 嘗試訪問 null 對象成員,空指針異常
ClassNotFoundException 不能載入所需的類
NumberFormatException 數字轉化格式異常,比如字元串到 float 型數字的轉換無效
IOException I/O 異常的根類
FileNotFoundException 找不到文件
EOFException 文件結束
InterruptedException 執行緒中斷
IllegalArgumentException 方法接收到非法參數
ClassCastException 類型轉換異常
SQLException 操作資料庫異常

 

四、異常處理機制

在java應用程式中,有兩種異常處理機制:拋出異常、捕獲異常。

聲明異常拋出異常:

  當一個方法出現錯誤引發異常時,方法創建異常對象交付運行時系統,異常對象中包含了異常類型和異常出現時程式的狀態等異常資訊。運行時系統負責尋找處置異常的程式碼並執行。

  可以通過 throws 關鍵字在方法上聲明該方法要拋出的異常,然後在方法內部通過 throw 拋出異常對象。本節詳細介紹在 Java 中如何聲明異常和拋出異常。

throws 關鍵字和 throw 關鍵字在使用上的幾點區別如下:

  • throws 用來聲明一個方法可能拋出的所有異常資訊,throw 則是指拋出的一個具體的異常類型。
  • 通常在一個方法(類)的聲明處通過 throws 聲明方法(類)可能拋出的異常資訊,而在方法(類)內部通過 throw 聲明一個具體的異常資訊。
  • throws 通常不用顯示地捕獲異常,可由系統自動將所有捕獲的異常資訊拋給上級方法; throw 則需要用戶自己捕獲相關的異常,而後再對其進行相關包裝,最後將包裝後的異常資訊拋出。

throws 聲明異常

  當一個方法產生一個它不處理的異常時,那麼就需要在該方法的頭部聲明這個異常,以便將該異常傳遞到方法的外部進行處理。可以使用 throws 關鍵字在方法的頭部聲明一個異常,其具體格式如下:

returnType method_name(paramList) throws Exception 1,Exception2,…{…}

其中,returnType 表示返回值類型,method_name 表示方法名,Exception 1,Exception2,… 表示異常類。如果有多個異常類,它們之間用逗號分隔。這些異常類可以是方法中調用了可能拋出異常的方法而產生的異常,也可以是方法體中生成並拋出的異常。

例 1

創建一個 readFile() 方法,該方法用於讀取文件內容,在讀取的過程中可能會產生 IOException 異常,但是在該方法中不做任何的處理,而將可能發生的異常交給調用者處理。在 main() 方法中使用 try catch 捕獲異常,並輸出異常資訊。程式碼如下:

 

import java.io.FileInputStream;  import java.io.IOException;  public class Test04  {      public void readFile() throws IOException      {          //定義方法時聲明異常          FileInputStream file=new FileInputStream("read.txt");    //創達 FileInputStream 實例對象          int f;          while((f=file.read())!=-1)          {              System.out.println((char)f);              f=file.read();          }          file.close();      }      public static void main(String[] args)      {          Throws t=new Test04();          try          {              t.readFile();    //調用 readFHe()方法          }          catch(IOException e)          {    //捕獲異常              System.out.println(e);          }      }  }

以上程式碼,首先在定義 readFile() 方法時用 throws 關鍵字聲明在該方法中可能產生的異常,然後在 main() 方法中調用 readFile() 方法,並使用 catch 語句捕獲產生的異常。

注意:在編寫類繼承程式碼時要注意,子類在覆蓋父類帶 throws 子句的方法時,子類的方法聲明中的 throws 子句不能出現父類對應方法的 throws 子句中沒有的異常類型,因此 throws 子句可以限制子類的行為。也就是說,子類方法拋出的異常不會超過父類定義的範圍。

throw 拋出異常

throw 語句用來直接拋出一個異常,後接一個可拋出的異常類對象,其語法格式如下:

throw ExceptionObject;

其中,ExceptionObject 必須是 Throwable 類或其子類的對象。如果是自定義異常類,也必須是 Throwable 的直接或間接子類。例如,以下語句在編譯時將會產生語法錯誤:

  1. throw new String(“拋出異常”); //因為String類不是Throwable類的子類

當 throw 語句執行時,它後面的語句將不執行,此時程式轉向調用者程式,尋找與之相匹配的 catch 語句,執行相應的異常處理程式。如果沒有找到相匹配的 catch 語句,則再轉向上一層的調用程式。這樣逐層向上,直到最外層的異常處理程式終止程式並列印出調用棧情況。

例 2

在某倉庫管理系統中,要求管理員的用戶名需要由 8 位以上的字母或者數字組成,不能含有其他的字元。當長度在 8 位以下時拋出異常,並顯示異常資訊;當字元含有非字母或者數字時,同樣拋出異常,顯示異常資訊。程式碼如下:

import java.util.Scanner;  public class Test05  {      public boolean validateUserName(String username)      {          boolean con=false;          if(username.length()>8)          {    //判斷用戶名長度是否大於8位              for(int i=0;i<username.length();i++)              {                  char ch=username.charAt(i);    //獲取每一位字元                  if((ch>='0'&&ch<='9')||(ch>='a'&&ch<='z')||(ch>='A'&&ch<='Z'))                  {                      con=true;                  }                  else                  {                      con=false;                      throw new IllegalArgumentException("用戶名只能由字母和數字組成!"");                  }              }          }          else          {              throw new IllegalArgumentException("用戶名長度必須大於 8 位!");          }          return con;      }      public static void main(String[] args)      {          Test05 te=new Test05();          Scanner input=new Scanner(System.in);          System.out.println("請輸入用戶名:");          String username=input.next();          try          {              boolean con=te.validateUserName(username);              if(con)              {                  System.out.println("用戶名輸入正確!");              }          }          catch(IllegalArgumentException e)          {              System.out.println(e);          }      }  }

 

如上述程式碼,在 validateUserName() 方法中兩處拋出了 IllegalArgumentException 異常,即當用戶名字元含有非字母或者數字以及長度不夠 8 位時。在 main() 方法中,調用了 validateUserName() 方法,並使用 catch 語句捕獲該方法可能拋出的異常。

運行程式,當用戶輸入的用戶名包含非字母或者數字的字元時,程式輸出異常資訊,如下所示。

請輸入用戶名:  administrator@#  java.lang.IllegalArgumentException: 用戶名只能由字母和數字組成!

 

當用戶輸入的用戶名長度不夠 8 位時,程式同樣會輸出異常資訊,如下所示。

請輸入用戶名:  admin  java.lang.IllegalArgumentException: 用戶名長度必須大於 8 位!

捕獲異常:

  在方法拋出異常後,運行時系統將轉為尋找合適的異常處理器。潛在的異常處理器是異常發生時依次存存留在調用棧中的方法的集合。當異常處理器所能處理的異常類型與方法拋出的異常類型相符時,即為合適的異常處理器。運行時系統從發生異常的方法開始,依次回查調用棧中的方法,直至找到合適的異常處理器的方法並執行。當運行時系統遍歷調用棧,而未找到合適的異常處理器方法時,運行時系統終止,同時意味著java程式的終止。

通常使用try、catch、finally來捕獲異常:

try  {      邏輯程式碼塊  }  catch(ExceptionType e)  {      異常處理程式碼塊  }  finally  {      清理程式碼塊  }

try塊:用於捕獲異常。其後可接零個或多個catch塊,如果沒有catch塊,則必須跟個finally塊。

catch塊:用於處理try捕獲到的異常。

finally塊:無論是否捕獲或處理異常,finally塊里的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally 語句塊將在方法返回之前被執行。

在以下4種特殊情況下,finally塊不會被執行:

1)在finally語句塊中發生了異常。

2)在前面的程式碼中用了System. exit(退出程式。

3)程式所在的執行緒死亡。

4)關閉CPU。

try、catch、finally 語句塊的執行順序:

1)當try沒有捕獲到異常時: try 語句塊中的語句逐一被執行,程式將跳過catch語句塊,執行finally語句塊和其後的語句;

2)當try捕獲到異常,catch語句塊里沒有處理此異常的情況:當try語句塊里的某條語句出現異常時,而沒有處理此異常的catch語句塊時,此異常將會拋給JVM處理,finally 語句塊里的語句還是會被執行,但finally語句塊後的語句不會被執行;

3)當try捕獲到異常,catch 語句塊里有處理此異常的情況:在try語句塊中是按照順序來執行的,當執行到某一條語句出現異常時,程式將跳到catch語句塊,並與catch語句塊逐一匹配,找到與之對應的處理程式,其他的catch語句塊將不會被執行,而try語句塊中,出現異常之後的語句也不會被執行,catch 語句塊執行完後,執行finally語句塊里的語句,最後執行finally語句塊後的語句。