1. 程式人生 > 其它 >JAVA自學第六天:異常機制

JAVA自學第六天:異常機制

異常概念

軟體程式在執行過程中,非常可能遇到剛剛提到的這些異常問題,我們叫異常,英文是:Exception,意思是例外。這些,例外情況,或者叫異常,怎麼讓我們寫的程式做出合理的處理。而不至於程式崩潰

異常指程式執行中出現的不期而至的各種狀況,如:檔案找不到、網路連線失敗、非法引數等。異常發生在程式執行期間,它影響了正常的程式執行流程。
比如說,你的程式碼少了一個分號,那麼執行出來結果是提示是錯誤java.lang.Error ;如果你用System.out.println(11/0) ,那麼你是因為你用0做了除數,會丟擲java.lang.ArithmeticException 的異常
異常發生的原因有很多,通常包含以下幾大類:

  • 使用者輸入了非法資料。
  • 要開啟的檔案不存在
  • 網路通訊時連線中斷,或者JVM記憶體溢位

這些異常有的是因為使用者錯誤引起,有的是程式錯誤引起的,還有其它一些是因為物理錯誤引起的

要理解Java異常處理是如何工作的,你需要掌握以下三種類型的異常:

  • 檢查性異常:最具代表的檢查性異常是使用者錯誤或問題引起的異常,這是程式設計師無法預見的。例如要開啟一個不存在檔案時,一個異常就發生了,這些異常在編譯時不能被簡單地忽略
  • 執行時異常: 執行時異常是可能被程式設計師避免的異常。與檢查性異常相反,執行時異常可以在編譯時被忽略
  • 錯誤:錯誤不是異常,而是脫離程式設計師控制的問題。錯誤在程式碼中通常被忽略。例如,當棧溢位時,一個錯誤就發生了,它們在編譯也檢查不到的

異常指不期而至的各種狀況,如:檔案找不到、網路連線失敗、除0操作、非法引數等。異常是一個事件,它發生在程式執行期間,干擾了正常的指令流程

Java語言在設計的當初就考慮到這些問題,提出異常處理的框架的方案,所有的異常都可以用一個異常類來表示,不同型別的異常對應不同的子類異常(目前我們所說的異常包括錯誤概念),定義異常處理的規範,在JDK1.4 版本以後增加了異常鏈機制,從而便於跟蹤異常。

Java異常是一個描述在程式碼段中發生異常的物件,當發生異常情況時,一個代表該異常的物件被建立並且在導致該異常的方法中被丟擲,而該方法可以選擇自己處理異常或者傳遞該異常

異常體系結構

Java把異常當作物件來處理,並定義一個基類java.lang.Throwable

作為所有異常的超類
在Java API中已經定義了許多異常類,這些異常類分為兩大類,錯誤Error和異常Exception
Java異常層次結構圖:

從圖中可以看出所有異常型別都是內建類Throwable 的子類,因而Throwable 在異常類的層次結構的頂層

接下來Throwable 分成了兩個不同的分支,一個分支是Error,它表示不希望被程式捕獲或者是程式無法處理的錯誤。另一個分支是Exception,它表示使用者程式可能捕捉的異常情況或者說是程式可以處理的異常。

其中異常類Exception 又分為執行時異常( RuntimeException )和非執行時異常。Java異常又可以
分為不受檢查異常( Unchecked Exception)和檢查異常( Checked Exception

異常之間的區別與聯絡

1、Error

Error 類物件由 Java 虛擬機器生成並丟擲,大多數錯誤與程式碼編寫者所執行的操作無關
比如說:
Java虛擬機器執行錯誤( Virtual MachineError ),當JVM不再有繼續執行操作所需的記憶體資源時,將出現 OutOfMemoryError 。這些異常發生時,Java虛擬機器(JVM)一般會選擇執行緒終止;

還有發生在虛擬機器試圖執行應用時,如類定義錯誤( NoClassDefFoundError )、連結錯誤( LinkageError )。這些錯誤是不可查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程式執行時不允許出現的狀況。

對於設計合理的應用程式來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在Java中,錯誤通常是使用Error 的子類描述。

2、Exception

Exception 分支中有一個重要的子類RuntimeException (執行時異常),該型別的異常自動為你所編寫的程式定義ArrayIndexOutOfBoundsException (陣列下標越界)、NullPointerException (空指標異常)、ArithmeticException (算術異常)、MissingResourceException (丟失資源)、ClassNotFoundException (找不到類)等異常,這些異常是不檢查異常,程式中可以選擇捕獲處理,也可以不處理。

這些異常一般是由程式邏輯錯誤引起的,程式應該從邏輯角度儘可能避免這類異常的發生;而RuntimeException 之外的異常我們統稱為非執行時異常,型別上屬於Exception 類及其子類,

從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOExceptionSQLException 等以及使用者自定義的Exception 異常,一般情況下不自定義檢查異常。

注意: ErrorException 的區別: Error 通常是災難性的致命的錯誤,是程式無法控制和處理的,當出現這些異常時,Java虛擬機器(JVM)一般會選擇終止執行緒; Exception 通常情況下是可以被程式處理的,並且在程式中應該儘可能的去處理這些異常

3、檢查異常和不受檢查異常

檢查異常:在正確的程式執行過程中,很容易出現的、情理可容的異常狀況,在一定程度上這種異常的發生是可以預測的,並且一旦發生該種異常,就必須採取某種方式進行處理

解析:除了RuntimeException及其子類以外,其他的Exception類及其子類都屬於檢查異常,當程式中可能出現這類異常,要麼使用try-catch語句進行捕獲,要麼用throws子句丟擲,否則編譯無法通過
不受檢查異常:包括RuntimeException及其子類和Error

分析: 不受檢查異常為編譯器不要求強制處理的異常, 檢查異常則是編譯器要求必須處置的異常。

Java異常處理機制

java異常處理本質:丟擲異常和捕獲異常

1、丟擲異常

要理解丟擲異常,首先要明白什麼是異常情形(exception condition),它是指阻止當前方法或作用域繼續執行的問題。其次把異常情形和普通問題相區分,普通問題是指在當前環境下能得到足夠的資訊,總能處理這個錯誤。

對於異常情形,已經無法繼續下去了,因為在當前環境下無法獲得必要的資訊來解決問題,你所能做的就是從當前環境中跳出,並把問題提交給上一級環境,這就是丟擲異常時所發生的事情。丟擲異常後,會有幾件事隨之發生。

首先,是像建立普通的java物件一樣將使用new 在堆上建立一個異常物件;然後,當前的執行路徑(已經無法繼續下去了)被終止,並且從當前環境中彈出對異常物件的引用。此時,異常處理機制接管程式,並開始尋找一個恰當的地方繼續執行程式

這個恰當的地方就是異常處理程式或者異常處理器,它的任務是將程式從錯誤狀態中恢復,以使程式要麼換一種方式執行,要麼繼續執行下去

舉例:
假使我們建立了一個學生物件Student的一個引用stu,在呼叫的時候可能還沒有初始化。所以在使用這個物件引用呼叫其他方法之前,要先對它進行檢查,可以建立一個代表錯誤資訊的物件,並且將它從當前環境中丟擲,這樣就把錯誤資訊傳播到更大的環境中。

Student stu = new Student();
    if (stu==null){
        throw new NullPointerException();

2、捕獲異常

在方法丟擲異常之後,執行時系統將轉為尋找合適的異常處理器(exception handler)。潛在的異常處理器是異常發生時依次存留在呼叫棧中的方法的集合。當異常處理器所能處理的異常型別與方法丟擲的異常型別相符時,即為合適的異常處理器。執行時系統從發生異常的方法開始,依次回查呼叫棧中的方法,直至找到含有合適異常處理器的方法並執行。當執行時系統遍歷呼叫棧而未找到合適的異常處理器,則執行時系統終止。同時,意味著Java程式的終止。

注意:
對於執行時異常錯誤檢查異常,Java技術所要求的異常處理方式有所不同

由於執行時異常及其子類的不可查性,為了更合理、更容易地實現應用程式,Java規定,執行時異常將由Java執行時系統自動丟擲,允許應用程式忽略執行時異常。

對於方法執行中可能出現的Error ,當執行方法不欲捕捉時,Java允許該方法不做任何丟擲宣告。因
為,大多數Error 異常屬於永遠不能被允許發生的狀況,也屬於合理的應用程式不該捕捉的異常

對於所有的檢查異常,Java規定:一個方法必須捕捉,或者宣告丟擲方法之外。也就是說,當一個方法
選擇不捕捉檢查異常時,它必須宣告將丟擲異常。

3、異常處理五個關鍵字

分別是: trycatchfinallythrowthrows

  • try -- 用於監聽。將要被監聽的程式碼(可能丟擲異常的程式碼)放在try語句塊之內,當try語句塊內發生異常時,異常就被丟擲。
  • catch* -- 用於捕獲異常。catch用來捕獲try語句塊中發生的異常
  • finally -- finally語句塊總是會被執行。它主要用於回收在try塊裡開啟的物力資源(如資料庫連線、網路連線和磁碟檔案)。只有finally塊,執行完成之後,才會回來執行try或者catch塊中的return或者throw語句,如果finally中使用了return或者throw等終止方法的語句,則就不會跳回執行,直接停止。
  • throw -- 用於丟擲異常
  • throws -- 用在方法簽名中,用於宣告該方法可能丟擲的異常

處理異常

1、try -catch

 try{

   }catch (Exception e){

   }catch (Exception e){

   }

要明白異常捕獲,還要理解監控區域(guarded region)的概念。它是一段可能產生異常的程式碼,並且後面跟著處理這些異常的程式碼。
因而可知,上述try-catch 所描述的即是監控區域,關鍵詞try 後的一對大括號將一塊可能發生異常的程式碼包起來,即為監控區域。Java方法在執行過程中發生了異常,則建立異常物件

將異常丟擲監控區域之外,由Java執行時系統負責尋找匹配的catch 子句來捕獲異常。若有一個catch 語句匹配到了,則執行該catch 塊中的異常處理程式碼,就不再嘗試匹配別的catch 塊了。

匹配原則:如果丟擲的異常物件屬於catch 子句的異常類,或者屬於該異常類的子類,則認為生成
的異常物件與catch 塊捕獲的異常型別相匹配。

【演示】

 public static void main(String[] args) {
        int a=1;
        int b=0;
        try {
            if (b==0) throw new ArithmeticException();
            System.out.println(a/b);
        }catch (ArithmeticException e){
            System.out.println("b不能等於0");
        }
        System.out.println("程式正常結束");
    }

注意:顯示一個異常的描述, Throwable 過載了toString() 方法(由Object 定義),所以
它將返回一個包含異常描述的字串。例如,將前面的catch 塊重寫成:

catch (ArithmeticException e){
            System.out.println("出現異常"+e);
        }

算術異常屬於執行時異常,因而實際上該異常不需要程式丟擲,執行時系統自動丟擲。如果不用trycatch
程式就不會往下執行了
【演示】

public static void main(String[] args) {
       int a=1;
       int b=0;
        System.out.println(a/b);
	System.out.println("1111");
    }


使用多重的catch語句:很多情況下,由單個的程式碼段可能引起多個異常。處理這種情況,我們需要定義兩個或者更多的catch 子句,每個子句捕獲一種型別的異常,當異常被引發時,每個catch 子句被依次檢查,第一個匹配異常型別的子句執行,當一個catch 子句執行以後,其他的子句將不會執行。

編寫多重catch語句塊注意事項
順序問題:先小後大,即先子類後父類
注意:
Java通過異常類描述異常型別。對於有多個catch 子句的異常程式而言,應該儘量將捕獲底層異常類的catch 子句放在前面,同時儘量將捕獲相對高層的異常類的catch 子句放在後面。否則,捕獲底層異常類的catch 子句將可能會被遮蔽

巢狀try語句try 語句可以被巢狀。也就是說,一個try 語句可以在另一個try 塊的內部。每
次進入try 語句,異常的前後關係都會被推入堆疊。如果一個內部的try 語句不含特殊異常的
catch 處理程式,堆疊將彈出,下一個try 語句的catch 處理程式將檢查是否與之匹配。這個
過程將繼續直到一個catch 語句被匹配成功,或者是直到所有的巢狀try 語句被檢查完畢。如果
沒有catch 語句匹配,Java執行時系統將處理這個異常。

 public static void main(String[] args) {
     try {
         int a = args.length;
         int b = 42/a;
         try{
             if (a==1){
                 a=a/(a-a);
             }
             if (a==2){
                 int c[] = {1};
                 c[42] = 99;
             }
         }catch (ArrayIndexOutOfBoundsException e){
             System.out.println("出現異常"+e);
         }

     }catch (ArithmeticException e){
         System.out.println("出現異常"+e);
     }
    }

當a=1時

當a=2時

分析:正如程式中所顯示的,該程式在一個try塊中嵌套了另一個try 塊。程式工作如下:當你在沒有命令列引數的情況下執行該程式,外面的try 塊將產生一個被0除的異常

程式在有一個命令列引數條件下執行,由巢狀的try 塊產生一個被0除的異常,由於內部的catch塊不匹配這個異常,它將把異常傳給外部的try 塊,在外部異常被處理。如果你在具有兩個命令列引數的條件下執行該程式,將由內部try 塊產生一個數組邊界異常。
注意:當有方法呼叫時, try 語句的巢狀可以很隱蔽的發生。例如,我們可以將對方法的呼叫放在一個try 塊中。在該方法的內部,有另一個try 語句。
在這種情況下,方法內部的try 仍然是巢狀在外部呼叫該方法的try 塊中的。下面我們將對上述例子進行修改,巢狀的try 塊移到方法nesttry()的內部:結果依舊相同

class NestTry{
static void nesttry(int a){
try{
if(a == 1){
a = a/(a-a);
}
if(a == 2){
int c[] = {1};
c[42] =99;
}
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("ArrayIndexOutOfBounds :"+e);
}
}
public static void main(String[] args){
try{
int a = args.length;
int b = 42 / a;
System.out.println("a = "+ a);
nesttry(a);
}catch(ArithmeticException e){
System.out.println("Divide by 0"+ e);
}
}
}

2、thorw

到目前為止,我們只是獲取了被Java執行時系統引發的異常。然而,我們還可以用throw 語句丟擲明確的異常。

throw ThrowableInstance

這裡的ThrowableInstance一定是Throwable 類型別或者Throwable 子類型別的一個物件。簡單的資料型別,例如intchar ,以及非Throwable 類,例如StringObject ,不能用作異常。

有兩種方法可以獲取Throwable 物件:在catch 子句中使用引數或者使用new 操作符建立。程式執行完throw 語句之後立即停止; throw 後面的任何語句不被執行,最鄰近的try 塊用來檢查它是否含有一個與異常型別匹配的catch 語句。

如果發現了匹配的塊,控制轉向該語句;如果沒有發現,次包圍的try 塊來檢查,以此類推。如果沒有發現匹配的catch 塊,預設異常處理程式中斷程式的執行並且列印堆疊軌跡。

class TestThrow{
static void proc(){
try{
throw new NullPointerException("demo");
}catch(NullPointerException e){
System.out.println("Caught inside proc");
throw e;
}
}
public static void main(String [] args){
try{
proc();
}catch(NullPointerException e){
System.out.println("Recaught: "+e);
}
}
}

該程式兩次處理相同的錯誤,首先, main() 方法設立了一個異常關係然後呼叫proc()。 proc()方法設立了另一個異常處理關係並且立即丟擲一個NullPointerException 例項NullPointerException 在main() 中被再次捕獲

該程式闡述了怎樣建立Java的標準異常物件,特別注意這一行:

throw new NullPointerException("demo");

分析:此處new 用來構造一個NullPointerException 例項,所有的Java內建的執行時異常有兩個構造方法:一個沒有引數,一個帶有一個字串引數

當用第二種形式時,引數指定描述異常的字串。如果物件用作print() 或者println() 的引數時,該字串被顯示。這同樣可以通過呼叫getMessage()來實現,getMessage()是由Throwable 定義的

3、throws

如果一個方法可以導致一個異常但不處理它,它必須指定這種行為以使方法的呼叫者可以保護它們自己而不發生異常。要做到這點,我們可以在方法宣告中包含一個throws 子句

一個throws 子句列舉了一個方法可能引發的所有異常型別。這對於除了ErrorRuntimeException 及它們子類以外型別的所有異常是必要的。一個方法可以引發的所有其他型別的異常必須在throws 子句中宣告,否則會導致編譯錯誤

public void info() throws Exception
{
//body of method
}

Exception 是該方法可能引發的所有的異常,也可以是異常列表,中間以逗號隔開

【例子】

class TestThrows{
static void throw1(){
System.out.println("Inside throw1 . ");
throw new IllegalAccessException("demo");
}
public static void main(String[] args){
throw1();
}
}

該例子中存在兩個錯誤,首先,throw1()方法不想處理所導致的異常,因而它必須宣告throws 子句來列舉可能引發的異常即IllegalAccessException ;其次, main() 方法必須定義try/catch 語句來捕獲該異常。

正確例子如下:

class TestThrows{
static void throw1() throws IllegalAccessException {
System.out.println("Inside throw1 . ");
throw new IllegalAccessException("demo");
}
public static void main(String[] args){
try {
throw1();
}catch(IllegalAccessException e ){
System.out.println("Caught " + e);
}
}
}

throws 丟擲異常的規則

  • 如果是不受檢查異常( unchecked exception ),即ErrorRuntimeException 或它們的子類,那麼可以不使用throws 關鍵字來宣告要丟擲的異常,編譯仍能順利通過,但在執行時會被系統丟擲。
  • 必須宣告方法可丟擲的任何檢查異常( checked exception )。即如果一個方法可能出現受可查異常,要麼用try-catch 語句捕獲,要麼用throws 子句宣告將它丟擲,否則會導致編譯錯誤
  • 僅當丟擲了異常,該方法的呼叫者才必須處理或者重新丟擲該異常。當方法的呼叫者無力處理該異常的時候,應該繼續丟擲,而不是囫圇吞棗
  • 呼叫方法必須遵循任何可查異常的處理和宣告規則。若覆蓋一個方法,則不能宣告與覆蓋方法不同的異常。宣告的任何異常必須是被覆蓋方法所宣告異常的同類或子類

finally

當異常發生時,通常方法的執行將做一個陡峭的非線性的轉向,它甚至會過早的導致方法返回。例如,如果一個方法打開了一個檔案並關閉,然後退出,你不希望關閉檔案的程式碼被異常處理機制旁路。

finally關鍵字為處理這種意外而設計
finally 建立的程式碼塊在try/catch 塊完成之後另一個try/catch 出現之前執行
finally 塊無論有沒有異常丟擲都會執行。如果丟擲異常,即使沒有catch 子句匹配finally 也會執行.
一個方法將從一個try/catch 塊返回到呼叫程式的任何時候,經過一個未捕獲的異常或者是一個明確的返回語句, finally 子句在方法返回之前仍將執行。這在關閉檔案控制代碼和釋放任何在方法開始時被分配的其他資源是很有用
注意: finally 子句是可選項,可以有也可以無,但是每個try 語句至少需要一個catch 或者finally 子句。

try, catch,finally ,return 執行順序

  1. 執行try,catch , 給返回值賦值
  2. 執行finally
  3. return

自定義異常

使用Java內建的異常類可以描述在程式設計時出現的大部分異常情況。除此之外,使用者還可以自定義異常。使用者自定義異常類,只需繼承Exception 類即可。
在程式中使用自定義異常類,大體可分為以下幾個步驟:

  • 建立自定義異常類
  • 在方法中通過throw 關鍵字丟擲異常物件
  • 如果在當前丟擲異常的方法中處理異常,可以使用try-catch 語句捕獲並處理;否則在方法的宣告處通過throws 關鍵字指明要丟擲給方法呼叫者的異常,繼續進行下一步操作
  • 在出現異常方法的呼叫者中捕獲並處理異常

總結