1. 程式人生 > >Java基礎系列6:深入理解Java異常體系

Java基礎系列6:深入理解Java異常體系

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。

 

前言:

Java的基本理念是“結構不佳的程式碼不能執行”。

“異常”這個詞有“我對此感到意外”的意思。問題出現了,你也許不清楚該如何處理,但你的確知道不應該置之不理;你要停下來,看看是不是有別人或在別的地方,能夠處理這個問題。只是在當前的環境中還沒有足夠的資訊來解決這個問題,所以就把這個問題提交到一個更高級別的環境中,在這裡將作出正確的決定。

使用異常所帶來的另一個相當明顯的好處是,它往往能夠降低錯誤處理程式碼的複雜度。如果不使用異常,那麼就必須檢查特定的錯誤,並在程式中的許多地方去處理它。而如果使用異常,那就不必在方法呼叫處進行檢查,因為異常機制將保證能夠捕獲這個錯誤。並且,只需在一個地方處理錯誤,即所謂的異常處理程式中。這種方式不僅節省程式碼,而且把“描述在正常執行過程中做什麼事”的程式碼和“出了問題怎麼辦”的程式碼相分離。總之,與以前的錯誤處理方法相比,異常機制使程式碼的閱讀、編寫和除錯工作更加井井有條。

 

異常概述:

現在我們需要編寫一個五子棋程式,當用戶輸入下期座標時,程式要判斷使用者輸入是否合法,如果保證程式有較好的容錯性,將會有如下的程式碼(虛擬碼):

if(使用者輸入包含除逗號之外的其他非數字字元)
{
	alert 座標只能是數值
	goto retry
}
else if(使用者輸入不包含逗號) {
	alert 應使用逗號分隔兩個座標值
	goto retry
}

else if(使用者輸入座標值超出了有效範圍) {
	alert 使用者輸入座標應位於棋盤座標之內
	goto retry
}

else if(使用者輸入的座標已有棋子)
{
	alert 只能在沒有棋子的地方下棋
	goto retry
}
else {
	.....
}

  

上面程式碼還未涉及任何有效處理,只是考慮了4種可能的錯誤,程式碼就已經急劇增加了。但實際上,上面考慮的4種情形還遠未考慮到所有的可能情形(事實上,世界上的意外是不可窮舉的),程式可能發生的異常情況總是大於程式設計師所能考慮的意外情況。

對於上面的錯誤處理機制,主要有以下兩個缺點:

  • 無法窮舉所有的異常情況。因為人類知識的限制,異常情況總比可以考慮到的情況多,總有“漏網之魚”的異常情況,所以程式總是不夠健壯。
  • 錯誤處理程式碼和業務實現程式碼混雜。這種錯誤處理和業務實現混雜的程式碼嚴重影響程式的可讀性,會增加程式維護的難度。

我們希望有這樣一種處理機制:

if(使用者輸入的資料不合法){
    .....
}else{
   處理邏輯
   .....  
}

  

上面偽碼提供了一個非常強大的“if塊”——程式不管輸入錯誤的原因是什麼,只要使用者輸入不滿足要求,程式就一次處理所有的錯誤。這種處理方法的好處是,使得錯誤處理程式碼變得更有條理,只需在一個地方處理錯誤。

這就需要用到java異常了。

 

異常是程式中的一些錯誤,但並不是所有的錯誤都是異常,並且錯誤有時候是可以避免的。

比如說,你的程式碼少了一個分號,那麼執行出來結果是提示是錯誤java.lang.Error;如果你用System.out.println(11/0),那麼你是因為你用0做了除數,會丟擲java.lang.ArithmeticException的異常。

異常發生的原因有很多,通常包含以下幾大類:

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

 

Java中的異常有以下三種類型:

檢查異常:最具代表的檢查性異常是使用者錯誤或問題引起的異常,這是程式設計師無法預見的。例如要開啟一個不存在檔案時,一個異常就發生了,這些異常在編譯時不能被簡單地忽略。

執行異常:執行時異常是可能被程式設計師避免的異常。與檢查性異常相反,執行時異常可以在編譯時被忽略。

錯誤:錯誤不是異常,而是脫離程式設計師控制的問題。錯誤在程式碼中通常被忽略。例如,當棧溢位時,一個錯誤就發生了,它們在編譯也檢查不到的。

 

異常的體系結構:

我們先來統觀以下Java的異常體系結構圖:

 

 

Java的異常被分為兩大類:Checked異常和Runtime異常(執行時異常)。所有的RuntimeException類及其子類的例項被稱為Runtime異常;不是RuntimeException類及其子類的異常例項則被稱為Checked異常。

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

 

Throwable:

Java異常的頂級類,所有的類都繼承自Throwable

Error:

Error錯誤,一般是指與虛擬機器相關的問題,如系統崩潰、虛擬機器錯誤、動態連結失敗等,這種錯誤無法恢復或不可能捕獲,將導致應用程式中斷。通常應用程式無法處理這些錯誤,因此應用程式不應該試圖使用catch 塊來捕獲Error物件。
在定義該方法時,也無須在其throws子句中宣告該方法可能丟擲Error及其任何子類。

 

Exception:

Exception中異常主要分為兩大類,執行時異常和檢查異常。常見的執行時異常有ArrayIndexOutOfBoundsException(陣列下標越界)、NullPointerException(空指標異常)、ArithmeticException(算術異常)、ClassNotFoundException(型別找不到),這些異常時非檢查異常,程式可以選擇處理,也可以不處理,編譯器編譯時期並不會報錯。這些異常一般是由於程式邏輯錯誤引起的,所以建議程式設計師還是處理一下。除執行時異常外的所有異常我們都稱為非執行時異常,也是必須處理的異常,如果不出來,程式編譯會報錯。

 

Error和Exception的區別:

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

 

檢查異常:必須處理的異常

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

 

非檢查異常:可以不處理

包括RuntimeException及其子類和Error

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

 

異常處理機制:

Java的異常處理機制可以讓程式具有極好的容錯性,讓程式更加健壯。當程式執行出現意外情形時,系統會自動生成一個Exception物件來通知程式,從而實現將“業務功能實現程式碼”和“錯誤處理程式碼”分離,提供更好的可讀性。

java異常關鍵字:

  • try – 用於監聽。try後緊跟一個花括號括起來的程式碼塊(花括號不可省略),簡稱try塊,它裡面放置可能引發異常的程式碼,當try語句塊內發生異常時,異常就被丟擲。【監控區域】
  • catch – 用於捕獲異常。catch後對應異常型別和一個程式碼塊,用於處理try塊發生對應型別的異常。【異常處理程式】
  • finally – 用於清理資源,finally語句塊總是會被執行。 多個catch塊後還可以跟一個finally塊,finally塊用於回收在try塊裡開啟的物理資源(如資料庫連線、網路連線和磁碟檔案)。只有finally塊執行完成之後,才會回來執行try或者catch塊中的return或者throw語句,如果finally中使用了return或者throw等終止方法的語句,則就不會跳回執行,直接停止。【使用finally進行清理】
  • throw – 用於丟擲一個實際的異常。throw可以單獨作為語句使用,丟擲一個具體的異常物件。【丟擲異常】
  • throws --用在方法簽名中,用於宣告該方法可能丟擲的異常。【異常說明】

 

1、使用try...catch捕獲異常:

語法格式:

try{
   //業務實現程式碼
   ...
}catch(Exception e){
  //異常處理程式碼
  ...
}

  

如果執行try塊裡的業務邏輯程式碼時出現異常,系統自動生成一個異常物件,該異常物件被提交給Java執行時環境,這個過程被稱為丟擲(throw)異常。

當Java執行時環境收到異常物件時,會尋找能處理該異常物件的catch塊,如果找到合適的catch塊,則把該異常物件交給該catch塊處理,這個過程被稱為捕獲(catch)異常;如果Java執行時環境找不到捕獲異常的catch塊,則執行時環境終止Java程式也將退出。

下面看幾個簡單的異常捕獲的例子:

例1:

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 e) {
			System.out.println("陣列越界,執行時引數不夠");
		}catch(NumberFormatException e) {
			System.out.println("數字格式異常");
		}catch(ArithmeticException e) {
			System.out.println("算術異常");
		}catch(Exception e) {
			System.out.println("未知異常");
		}
	}

}

  

  • 如果執行該程式時輸入的引數不夠,將會發生陣列越界異常,Java執行時將呼叫IndexOutOfBoundsException對應的catch塊處理該異常。
  • 如果執行該程式時輸入的引數不是數字,而是字母,將發生數字格式異常,Java執行時將呼叫NumberFormatException 對應的catch塊處理該異常。
  • 如果執行該程式時輸入的第二個引數是0,將發生除0異常,Java執行時將呼叫ArithmeticException對應的catch塊處理該異常。
  • 如果程式執行時出現其他異常,該異常物件總是Exception類或其子類的例項,Java執行時將呼叫Exception對應的catch塊處理該異常。

上面程式中的三種異常是我們在程式設計中經常遇見的,讀者應該掌握這些異常。

例2:

import java.util.Date;


public class Test {
	
	public static void main(String[] args) {
		Date d=null;
		try {
			System.out.println(d.after(new Date()));
		}catch(NullPointerException e) {
			System.out.println("空指標異常");
		}catch(Exception e) {
			System.out.println("未知異常");
		}
	}

}

  

上面程式針對NullPointerException異常提供了專門的異常處理塊。上面程式呼叫一個null物件的after0方法,這將引發NullPointerException異常(當試圖呼叫一個null物件的例項方法或例項變數時,就會引發NullPointerException異常),Java 執行時將會呼叫NullPointerException對應的catch塊來處理該異常;如果程式遇到其他異常,Java執行時將會呼叫最後的catch塊來處理異常。

catch塊處理異常遵循著:先小後大,即先子類後父類。正如在前面程式所看到的,程式總是把對應Exception類的catch塊放在最後,這是為什麼呢?讀者可能明白原因:如果把Exception類對應的catch塊排在其他catch塊的前面,Java執行時將直接進入該catch塊(因為所有的異常物件都是Exception或其子類的例項),而排在它後面的catch塊將永遠也不會獲得執行的機會。

 

多異常捕獲:

在Java7之前,每個catch塊只能捕獲一個異常,Java7之後,每個catch塊可以捕獲多種型別的異常。

上面的例1可以改成如下程式碼實現:

public class Test {
	
	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|NumberFormatException|ArithmeticException e) {
			System.out.println("陣列越界,數字格式異常,算術異常");
		}catch(Exception e) {
			System.out.println("未知異常");
		}
	}

}

  

2、使用throws宣告丟擲異常:

使用throws宣告丟擲異常的思路是,當前方法不知道如何處理這種型別的異常,該異常應該由上一級呼叫者處理;如果main方法也不知道如何處理這種型別的異常,也可以使用throws宣告丟擲異常,該異常將交給JVM處理。JVM對異常的處理方法是,列印異常的跟蹤棧資訊,並中止程式執行,這就是前面程式在遇到異常後自動結束的原因。

import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class Test {
	
	public static void main(String[] args) throws FileNotFoundException {
		FileInputStream fis=new FileInputStream("a.txt");
	}

}

  

上面程式宣告不處理IOException異常,將該異常交給JVM處理,所以程式一旦遇到該異常,JVM就會列印該異常的跟蹤棧資訊,並結束程式。執行上面程式,會看到如下圖所示的執行結果。

 

 

3、使用throw丟擲異常:

public class Test {
	
	public static void main(String[] args) throws Exception {
		try {
			int a=Integer.parseInt(args[0]);
			int b=Integer.parseInt(args[1]);
			int c=a/b;
			if(b==0) {
				throw new Exception("除數不能為0");
			}
			System.out.println("您輸入的兩個數相除的結果是"+c);
		}catch(Exception e) {
			System.out.println("未知異常");
		}
	}

}

  

上面程式中粗體字程式碼使用throw語句來自行丟擲異常。當Java執行時接收到開發者自行丟擲的異常時,同樣會中止當前的執行流,跳到該異常對應的catch塊,由該catch塊來處理該異常。也就是說,不管是系統自動丟擲的異常,還是程式設計師手動丟擲的異常,Java執行時環境對異常的處理沒有任何差別。

 

4、訪問異常資訊:

如果程式需要在catch塊中訪問異常物件的相關資訊,則可以通過訪問catch塊的後異常形參來獲得。當Java執行時決定呼叫某個catch塊來處理該異常物件時,會將異常物件賦給catch塊後的異常引數,程式即可通過該引數來獲得異常的相關資訊。

所有的異常物件都包括如下幾個常用的方法:

getMessage():返回該異常資訊的跟蹤棧資訊輸出到標準錯誤輸出

printStackTrace():將該異常的跟蹤棧資訊輸出到標準錯誤輸出。

printStackTrace(PrintStream s):將該異常的跟蹤棧資訊輸出到指定輸出流。

getStackTrace():返回該異常的跟蹤棧資訊。

 

5、使用finally回收資源:

有些時候,程式在try塊裡打開了一些物理資源(例如資料庫連線、網路連線和磁碟檔案),這些物理資源都必須顯示回收。

在哪裡回收這些物理資源呢?在try塊裡回收?還是在catch塊中進行回收?假設程式在try塊裡進行資源回收,根據圖10.1所示的異常捕獲流程—一如果try塊的某條語句引起了異常,該語句後的其他語句通常不會獲得執行的機會,這將導致位於該語句之後的資源回收語句得不到執行。如果在catch塊裡進行資源回收,但catch塊完全有可能得不到執行,這將導致不能及時回收這些物理資源。

為了保證一定能回收try塊中開啟的物理資源,異常處理機制提供了finally塊。不管try塊中的程式碼是否出現異常,也不管哪一個catch塊被執行,甚至在try塊或catch塊中執行了return語句,finally塊總會被執行。完整的Java異常處理語法結構如下:

try{
  //業務實現程式碼
  ...
}catch(SubException e){
  //異常處理塊
  ...
}catch(SubException e2){
   //異常處理塊
   ...
}finally{
   //資源回收
   ...
}

  

例如:

public class Test {
	
	public static void main(String[] args) throws Exception {
		try {
			int a=10;
			int b=0;
			int c=a/b;
		}catch(Exception e) {
			System.out.println("未知異常");
		}finally {
			System.out.println("資源回收");
		}
	}

}

  

結果:

未知異常
資源回收

  

 

總結:

 

 

&n