1. 程式人生 > >通過異常處理錯誤

通過異常處理錯誤

概念

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

如果在每次呼叫方法的時候都徹底地進行錯誤檢査,程式碼很可能會變得難以閱讀。對於構造大型、健壯、可維護的程式而言,這種錯誤處理模式將會成為主要障礙。

我們的做法應該是用強制規定的形式來消除錯誤處理過程中隨心所欲的因素。這種做法由來已久,對異常處理的實現可以追湖到20世紀60年代的作業系統,甚至於BASIC語言中的on error goto語句。而C++的異常處理機制基於Ada,Java中的異常處理則建立在C++的基礎之上(儘管看上去更像Object Pascal)。

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

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

基本異常

異常情形(exceptional condition)是指阻止當前方法或作用域繼續執行的問題。把異常情形與普通問題相區分很重要,所謂的普通同題是指,在當前環境下能得到足夠的資訊,總能處理這個錯誤。 而對於異常情形就不能繼續下去了,因為在當前環境下無法獲得必要的資訊來解決問題。你所能做的就是從當前環境跳出,並且把問題提交給上一級環境。這就是丟擲異常時所發生的事情。

當丟擲異常後有幾件事會隨之發生。首先,同Java中其他物件的建立一樣,將使用new在堆上建立異常物件。然後,當前的執行路徑(它不能繼續下去了)被終止,並且從當前壞境中彈出對異常物件的引用。此時異常處理機制接管程式,並開始尋找一個恰當的地方來繼續執行程式。這個恰當的地方就是異常處理程式,它的任務是將程式從錯誤狀態中恢復,以使程式能要麼換一種方式執行要麼繼續通行下去。

異常使得我們可以將每件事都當作一個事務來考慮,而異常可以看護著這些事務的底線,我們還可以將異常看作是一種內建的恢復(undo)系統,因為(在細心使用的情況下)我們在程式中可以擁有各種不同的恢復點。如果程式的某部分失敗了,異常將“恢復”到程式中某個已知的穩定點上。

異常最重要的方面之一就是如果發生問題,它們將不允許程式沿著其正常的路徑繼續走下去。

異常引數

與使用Java中的其他物件一樣,我們總是用new在堆上建立異常物件,這也伴隨著儲存空間的分配和構造器的呼叫。所有標準異常類都有兩個構造器:一個是預設構造器;另一個是接受字串作為引數,以便能把相關資訊放入異常物件的構造器;

throw new NullPointerException(“t == null”);

關鍵字throw將產生許多有趣的結果。在使用new建立了異常物件之後,此物件的引用將傳給throw。儘管返回的異常物件其型別通常與方法設計的返回型別不同,但從效果上看它就像是從方法“返回”的。可以簡單地把異常處理看成一種不同的返回機制,當然著過分強調這種類比的話,就會有麻煩了。另外還能用丟擲異常的方式從當前的作用域退出。在這兩種情況下,將會返回一個異常物件,然後退出方法或作用域。

丟擲異常與方法正常返回值的相似之處到此為止。因為異常返回的“地點”與普通方法呼叫返回的“地點”完全不同。(異常將在一個恰當的異常處理程式中得到解決,它的位置可能離異常被丟擲的地方很遠,也可能會跨越方法呼叫棧的許多層次。)

此外,能夠丟擲任意型別的Throwable物件,它是異常型別的根類。通常對於不同型別的錯誤,要丟擲相應的異常。錯誤資訊可以儲存在異常物件內部或者用異常類的名稱來暗示。上一層壞境通過這些資訊來決定如何處理異常。(通常異常物件中僅有的資訊就是異常型別,除此之外不包含任何有意義的內容。)

try塊

要明白異常是如何被捕獲的,必須首先理解監控區域(guarded region)的概念。它是一段可能產生異常的程式碼,並且後面跟著處理這些異常的程式碼。

如果在方法內部丟擲了異常(或者在方法內部呼叫的其他方法丟擲了異常),這個方法將在丟擲異常的過程中結束。要是不希望方法就此結束,可以在方法內設定一個特殊的塊來捕獲異常。因為在這個塊裡“嘗試”各種(可能產生異常的)方法呼叫,所以稱為try塊。它是跟在try 關鍵字之後的普通程式塊:

try {     // Code that might generate exceptions }

對於不支援異常處理的程式語言,要想仔細檢査錯誤,就得在每個方法呼叫的前後加上設定和錯誤檢査的程式碼,甚至在每次呼叫同一方法時也得這麼做。有了異常處理機制,可以把所有動作都放在try塊裡,然後只需在一個地方就可以捕獲所有異常。這意味著程式碼將更容易編寫和閱讀,因為完成任務的程式碼沒有與錯誤檢査的程式碼混在一起。

異常處理程式

當然,丟擲的異常必須在某處得到處理。這個“地點”就是異常處理程式,而且針對每個要捕獲的異常得準備相應的處理程式。異常處理程式緊跟在try塊之後,以關鍵字catch表示:

try {     // Code that might generate exceptions } catch(Type1 id1) {     // Handle exceptions of Type1 } catch(type2 ld2) {     // Handle exceptions of Type1 }

每個catch子句(異常處理程式)看起來就像是接收一個且僅接收一個特殊型別的引數的方法。可以在處理程式的內部使用識別符號(id1, id2等等),這與方法引數的使用很相似。有時可能用不到識別符號,因為異常的型別已經給了你足夠的資訊來對異常進行處理,但識別符號並不可以省略。

異常處理程式必須緊跟在try塊之後。當異常被丟擲時,異常處理機制將負責搜尋引數與異常型別相匹配的第一個處理程式。然後進入catch子句執行,此時認為異常得到了處理。一旦catch子句結束,則處理程式的査找過程結束。注意,只有匹配的catch子句才能得到執行,這與switch語句不同,switch語句需要在每一個case後面跟一個break,以避免執行後續的case子句。

注意在try塊的內部,許多不同的方法呼叫可能會產生型別相同的異常,而你只需要提供一個針對此型別的異常處理程式。

終止與恢復

異常處理理論上有兩種基本模型。Java支援終止模型(它是Java和C++所支援的模型)。

在這種模型中,將假設錯誤非常關鍵,以至於程式無法返回到異常發生的地方繼續執行。一旦異常被丟擲,就表明錯誤已無法挽回,也不能回來繼續執行。

另一種稱為恢復模型。意思是異常處理程式的工作是修正錯誤,然後重新嘗試調用出問題的方法,並認為第二次能成功。對於恢復模型,通常希望異常被處理之後能繼續執行程式。如果想要用Java實現類似恢復的行為,那麼在遇見錯誤時就不能丟擲異常,而是呼叫方法來修正該錯誤。或者把try塊放在while迴圈裡,這樣就不斷地進入try,直到得到滿意的結果。

長久以來,儘管程式設計師們使用的作業系統支援恢復模型的異常處理,但他們最終還是轉向使用類似“終止模型”的程式碼,並且忽略恢復行為。所以雖然恢復模型開始顯得很吸引人,但不是很實用。其中的主要原因可能是它所導致的耦合:恢復性的處理程式需要了解異常批出的地點,這勢必要包含依賴於丟擲位置的非通用性程式碼。這增加了程式碼編寫和維護的困難,對於異常可能會從許多地方丟擲的大型程式來說,更是如此。

建立自定義異常

不必構泥於Java中已有的異常型別。Java提供的異常體系不可能預見所有的希望加以報告的錯誤,所以可以自己定義異常類來表示程式中可能會遇到的特定問題。

要自己定義異常類,必須從已有的異常類繼承,最好是選擇意思相近的異常類繼承(不過這樣的異常並不容易找)。建立新的異常型別最簡單的方法就是讓編譯器為你產生預設構造器,所以這幾乎不用寫多少程式碼:

class SimplException extends Exception{
	public SimplException() {}
	public SimplException(String str) {
		super(str);
	}
}
public class InheritingExceptions {
	public void f() throws SimplException{
		System.out.println("Throw SimplException from f()");
	}
	public void f2() throws SimplException{
		throw new SimplException("主動丟擲異常");
	}
	public static void main(String[] args) {
		InheritingExceptions i = new InheritingExceptions();
		try {
			i.f();
			i.f2();
		} catch (Exception e) {
			e.printStackTrace(System.out);
		}
	}
	//輸出:
	//Throw SimplException from f()
	//thinkinjava.SimplException: 主動丟擲異常
	//at InheritingExceptions.f2(InheritingExceptions.java:15)
	//at InheritingExceptions.main(InheritingExceptions.java:21)
}

兩個構造器定義了 SimplException 型別物件的建立方式。對於第二個構造器,使用super關鍵字呼叫了基類構造器,它接受一個字串作為引數。

異常與記錄日誌 你可能還想使用java.util.logging工具將輸出記錄到日誌中。

class LoggingException extends Exception{
	private static Logger logger = Logger.getLogger("LoggingException");
	public LoggingException() {
		StringWriter sw = new StringWriter();
		printStackTrace(new PrintWriter(sw));
		logger.severe(sw.toString());
	}
}

public class InheritingExceptions {
	public static void main(String[] args) {
		try {
			throw new LoggingException();
		} catch (Exception e) {
			System.err.println(e);
		}
	}
	//輸出:
	//月 24, 2018 8:53:28 上午 thinkinjava.LoggingException <init>
	//SEVERE: thinkinjava.LoggingException
	//at thinkinjava.InheritingExceptions.main(InheritingExceptions.java:32)
	//thinkinjava.LoggingException

}

異常說明

Java鼓勵人們把方法可能會丟擲的異常告知使用此方法的容戶端程式設計師。這是種優雅的做法,它使得呼叫者能確切知道寫什麼樣的程式碼可以捕獲所有潛在的異常。當然,如果提供了原始碼,客戶端程式設計師可以在原始碼中査找throw語句來獲知相關資訊,然而程式庫通常並不與原始碼一起釋出。為了預防這樣的問題,Java提供了相應的語法(並強制制使用這個語法),使你能以禮貌的方式告知客戶端程式設計師某個方法可能會丟擲的異常型別,然後客戶端程式設計師就可以進行相應的處理。這就是異常說明,它屬於方法宣告的一部分,緊跟在形式引數列表之後。

異常說明使用了附加的關鍵字throws,後面接一個所有潛在異常型別的列表,所以方法定義可能看起來像這樣:

void f() throws TooBig,TooSmall,DivZero{ //…

但是,要是這樣寫:

void f() { //…

就表示此方法不會丟擲任何異常(除了從RuntimeException繼承的異常,它們可以在沒有異常說明的情況下被丟擲,這些將在後面進行討論)。

程式碼必須與異常說明保持一致。如果方法裡的程式碼產生了異常卻沒有進行處理,編譯器會發現這個問題並提醒你,要麼處理這個異常,要麼就在異常說明中表明此方法將產生異常。通過這種自頂向下強制執行的異常說明機制,Java在編譯時就可以保證一定水平的異常正確性。

不過還是有個能“作弊”的地方:可以宣告方法將丟擲異常,實際上卻不丟擲。編譯器相信了這個宣告,並強制此方法的使用者像真的丟擲異常那樣使用這個方法。這樣做的好處是為異常先佔個位子,以後就可以丟擲這種異常而不用修改已有的程式碼。在定義抽象基類和介面時這種能力很重要,這樣派生類或介面實現就能夠丟擲這些預先宣告的異常。

這種在編譯時被強制檢査的異常稱為被檢查的異常

捕獲所有異常

可以只寫一個異常處理程式來捕獲所有型別的異常。通過捕獲異常型別的基類Exception就可以做到這一點(事實上還有其他的基類,但Exception是同程式設計活動相關的基類):

catch(Exception e1) {     System.out.println(“Caught an exception”); }

這將捕獲所有異常,所以最好把它放在處理程式列表的末尾,以防它搶在其他處理程式之前先把異常捕獲了。因為Exception是與程式設計有關的所有異常類的基類,所以它不會含有太多具體的資訊,不過可以呼叫它從其基類Throwable繼承的方法:

String getMessage() String getLocalizedMessage()

用來獲取詳細資訊,或用本地語言表示的詳細資訊。

String toString()

返回對Throwable的簡單描述,要是有詳細資訊的話,也會把它包含在內。

void printStackTrace() void printStackTrace(PrintStream) void printStackTrace(java.io.PrintWriter)

列印Throwable和Throwable的呼叫棧軌跡。呼叫棧顯示了“把你帶到異常丟擲地點”的方法呼叫序列。其中第一個版本輸出到標準錯誤,後兩個版本允許選擇要輸出的流。

Throwable fillInStackTrace()

用於在Throwable物件的內部記錄棧幀的當前狀態,這在程式重新丟擲錯誤或異常時很有用。

棧軌跡

PrintStackTrace()方法所提供的資訊可以通過getStackTrace()方法來直接訪問,這個方法將返回一個由棧軌跡中的元素所構成的陣列,其中每一個元素都表示棧中的一幀。元素0是棧頂元素並且是呼叫序列中的最後一個方法呼叫(這個Throwable被建立和丟擲之處)。陣列中的最後一個元素和棧底是呼叫序列中的第一個方法呼叫。下面是個例子:

public class WhoCalled {
	static void f(){
		try {
			throw new Exception();
		} catch (Exception e) {
			e.printStackTrace();
			for(StackTraceElement ste:e.getStackTrace()){
				System.out.println(ste.getMethodName());
			}
		}
	}
	static void g(){f();}
	static void h(){g();}
	public static void main(String[] args) {
		h();
	}
	//輸出:
	//java.lang.Exceptionf
	//g
	//h
	//main
	//...
}

重新丟擲異常

有時希望把剛捕獲的異常重新丟擲,尤其是在使用Exception捕獲所有異常的時候。既然已經得到了對當前異常物件的引用,可以直接把它重新丟擲:

catch(Exception e) {     System.out.println(“An exception was thrown”);     throw e: }

重拋異常會把異常拋給上一級環境中的異常處理程式,同一個try塊的後續catch子句將被忽略。此外異常物件的所有資訊都得以保持,所以高一級環境中捕獲此異常的處理程式可以從這個異常物件中得到所有資訊。

如果只是把當前異常物件重新丟擲,那麼printStackTrace()方法顯示的將是原來異常丟擲點的呼叫棧資訊,而並非重新丟擲點的資訊。要想更新這個資訊,可以呼叫fillInStacktrace()方法,這將返回一個Throwable物件,它是通過把當前呼叫棧資訊填入原來那個異常物件而建立的,就像這樣:

public class WhoCalled {
	static void f() throws Exception{
		throw new Exception("throw from f()");
	}
	static void g() throws Exception{
		f();
	}
	static void h()throws Exception{
		try {
			g();
		} catch (Exception e) {
			throw (Exception)e.fillInStackTrace();
			//throw e;
		}
	}
	public static void main(String[] args) {
		try {
			h();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	//輸出:
	//java.lang.Exception: throw from f()
	//at thinkinjava.WhoCalled.h(WhoCalled.java:14)
	//at thinkinjava.WhoCalled.main(WhoCalled.java:19)
	
	//如果使用throw e;丟擲則輸出:
	//java.lang.Exception: throw from f()
	//at thinkinjava.WhoCalled.f(WhoCalled.java:5)
	//at thinkinjava.WhoCalled.g(WhoCalled.java:8)
	//at thinkinjava.WhoCalled.h(WhoCalled.java:12)
	//at thinkinjava.WhoCalled.main(WhoCalled.java:20)
}

異常鏈

常常會想要在捕獲一個異常後丟擲另一個異常,並且希望把原始異常的資訊儲存下來,這被稱為異常鏈。在JDK1.4以前,程式設計師必須自己編寫程式碼來儲存原始異常的資訊。現在所有Throwable的子類在構造器中都可以接受一個cause(因由)物件作為引數。這個cause就用來表示原始異常,這樣通過把原始異常傳進給新的異常,使得即使在當前位置建立並丟擲了新的異常,也能通過這個異常鏈追蹤到異常最初發生的位置。

有趣的是,在Throwable的子類中,只有三種基本的異常類提供了帶cause引數的構造器。它們是Error(用於Java虛擬機器報告系統錯誤)、Exception以及RuntimeException。如果要把其他型別的異常連結起來,應該使用initCause()方法而不是構造器。 例:

public class WhoCalled2 {
	static void f(){
		try {
			throw new Exception();
		} catch (Exception e) {
			RuntimeException re = new RuntimeException();
			re.initCause(e);
			throw re;
		}
	}

	public static void main(String[] args) {
		f();
	}
	//輸出:
	//Exception in thread "main" java.lang.RuntimeException
	//		at thinkinjava.WhoCalled2.f(WhoCalled2.java:9)
	//		at thinkinjava.WhoCalled2.main(WhoCalled2.java:16)
	//Caused by: java.lang.Exception
	//		at thinkinjava.WhoCalled2.f(WhoCalled2.java:7)
	//		... 1 more

}

Java標準異常

Throwable這個Java類被用來表示任何可以作為異常被丟擲的類。Throwable物件可分為兩種型別(指從Throwable繼承而得到的型別):Error用來表示編譯時和系統錯誤(除特殊情況外,一般不用你關心),Exception是可以被丟擲的基本型別,在Java類庫、使用者方法以及執行時故障中都可能丟擲Exception型異常。所以Java程式設計師關心的基型別通常是Exception。

要想對異常有全面的瞭解,最好去瀏覽一下HTML格式的Java文件(可以從java.sun.com下載)。為了對不同的異常有個感性的認識,這樣做是值得的。但很快你就會發現,這些異常除了名稱外其實都差不多。同時,Java中異常的數目在持續增加,所以在這裡羅列它們毫無意義。所使用的第三方類庫也可能會有自己的異常。對異常來說,關鍵是理解概念以及如何使用。

異常的基本的概念是用名稱代表發生的問題,並且異常的名稱應該可以望文知意。異常並非全是在java.lang包裡定義的。有些異常是用來支援其他像util、net和io這樣的程式包,這些異常可以通過它們的完整名稱或者從它們的父類中看出端倪。比如,所有的輸入/輸出異常都是從 java.io.IOException繼承而來的。

特例RuntimeException

在上文中你會發現這樣一個異常:

throw new NullPointerException();

如果必須對傳遞給方法的每個引用都檢査其是否為null(因為無法確定呼叫者是否傳入了非法引用),這聽起來著實嚇人。幸運的是,這不必由你親自來做,它屬於Java的標準通行時檢測的一部分。如果對null引用進行呼叫,Java會自動丟擲NullPointerException異常,所以上述程式碼是多餘的,儘管你也許想要執行其他的檢査以確保NullPointerException不會出現。

屬於執行時異常的型別有很多,它們會自動被Java虛擬機器丟擲所以不必在異常說明中把它們列出來。這些異常都是從RuntimeException類繼承而來,所以既體現了繼承的優點,使用起來也很方便。這構成了一組具有相同特徵和行為的異常型別。並且,也不再需要在異常說明中宣告方法將丟擲RuntimeException型別的異常(或者任何從RuntimeException繼承的異常),它們也被稱為“不受檢查異常”。這種異常屬於錯誤,將被自動捕獲,就不用你親自動手了。要是自己去檢査RuntimeExcePtion的話,程式碼就顯得太混亂了。不過儘管通常不用捕獲 RuntimeExcePtion異常,但還是可以在程式碼中丟擲RuntimeException型別的異常。

請務必記住:只能在程式碼中忽略RuntimeException(及其子類)型別的異常,其他型別異常的處理部是由編譯器強制實施的。究其原因,RuntimeException代表的是程式設計錯誤:

  1. 無法預料的錯誤。比如從你控制範圍之外傳遞進來的null引用。
  2. 作為程式設計師,應該在程式碼中進行檢査的錯誤。

你會發現在這些情況下使用異常很有好處, 它們能給除錯帶來便利。

值得注意的是:不應把Java的異常處理機制當成是單一用途的工具。是的,它被設計用來處理一些煩人的執行時錯誤,這些錯誤往往是由程式碼控制能力之外的因素導致的,然而它對於發現某些編譯器無法檢測到的程式設計錯誤也是非常重要的。

使用finally進行清理

對於一些程式碼,可能會希望無論try塊中的異常是否丟擲,它們都能得到執行。這通常適用於記憶體回收之外的情況(因為回收由垃圾回收器完成)。為了達到這個效果,可以在異常處理程式後面加上finally語句。完整的異常處理程式看起來像這樣:

try {     // The guarded region: Dangerous activities } catch(A a1){     // Handler for situation A } catch(B b1) {     // Handler for situation B } finally {     // Activities that happen every time

對於沒有垃圾回收和解構函式自動呼叫機制的語言來說,finally非常重要。它能使程式設計師保證:無論try塊裡發生了什麼,記憶體總能得到釋放。但Java有垃圾回收機制,所以記憶體解放不再是問題。而且Java也沒有解構函式可供呼叫。那麼,Java在什麼情況下才能用到finally呢?

當要把除記憶體之外的資源恢復到它們的初始狀態時,就要用到finally子句。這種需要清理的資源包括:已經開啟的檔案或網路連線,在螢幕上畫的圖形,甚至可以是外部世界的某個開關,如下面例子所示:

public class OnOff {

	private boolean state = false;
	public void on(){state = true;}
	public void off(){state = false;}
	@Override
	public String toString() {
		return state?"on":"off";
	}
	public static void main(String[] args) {
		OnOff oo = new OnOff();
		try {
			oo.on();
			//do something
			throw new Exception();
		} catch (Exception e) {
			System.out.println(oo);
		}finally{
			oo.off();
			System.out.println(oo);
		}
	}
	//輸出:
	//on
	//off
}

上面的例子表示無論如何oo物件的某個開關在結束或丟擲異常之後都必須保證關閉狀態。

當涉及break和continue語句的時候,finally子句也會得到執行。請注意,如果把finally子句和帶標籤的break及continue配合使用,在Java裡就沒必要使用goto語句了。

在return中使用finally

因為finally子句總是會執行的,所以在一個方法中,可以從多個點返回,並且可以保證重要的清理工作仍舊會執行

public class Test{
	public static void f(int i){
		try {
			System.out.println("1");
			if(i == 1) return;
			System.out.println("2");
			if(i == 2) return;
			System.out.println("end");
		} finally {
			System.out.println("clear");
		}
	}
	public static void main(String[] args) {
		f(2);
	}
	//輸出:
	//1
	//2
	//clear

}

從輸出中可以看出,在finally類內部,從何處返回無關緊要。

缺憾:異常丟失

遺憾的是,Java的異常實現也有瑕疵。異常作為程式出錯的標誌,決不應該被忽略,但它還是有可能被輕易地忽略。用某些特殊的方式使用finally子句,就會發生這種情況。下面是一個簡單的異常丟失例子:

public class Test {
	public static void main(String[] args) {
		try {
			throw new RuntimeException();
		} finally {
			return;
		}
	}
}

如果執行這個程式,就會看到即使丟擲了異常,它也不會產生任何輸出。

構造器

有一點很重要,即你要時刻詢問白己“如果異常發生了,所有東西能被正確的清理嗎?”儘管大多數情況下是非常安全的,但涉及構造器時,問題就出現了。構造器會把物件設定成安全的初始狀態,但還會有別的動作,比如開啟一個檔案,這樣的動作只有在物件使用完畢並且使用者呼叫了特殊的清理方法之後才能得以清理。如果在構造器內丟擲了異常,這些清理行為也許就不能正常工作了。這意味著在編寫構造器時要格外細心。

也許使用finally就可以解決問題。但問題井非如此簡單,因為finally會每次都執行清理程式碼。如果構造器在其執行過程中半途而廢,也許該物件的某些部分還沒有被成功建立,而這些部分在finally子句中卻是要被清理的。

異常的限制

當覆蓋方法的時候,只能丟擲在基類方法的異常說明裡列出的那些異常。這個限制很有用,因為這意味著,當基類使用的程式碼應用到其派生類物件的時候,一樣能夠工作(當然,這是面向物件的基本概念),異常也不例外。

異常限制對構造器不起作用。因為基類構造器必須以這樣或那樣的方式被呼叫,派生類構造器的異常說明必須包含基類構造器的異常說明。派生類構造器不能捕獲基類構造器丟擲的異常

儘管在繼承過程中,編譯器會對異常說明做限制要求,但異常說明本身並不屬於方法型別的一部分,方法型別是由方法的名字與引數的裝型組成的。因此不能基於異常說明來過載方法。此外,一個出現在基類方法的異常說明中的異常,不一定會出現在派生類方法的異常說明裡。這點同繼承的規則明顯不同,在繼承中基類的方法必須出現在派生類裡,換句話說,在繼承和覆蓋的過程中,某個特定方法的“異常說明的介面不是變大了而是變小了——這恰好和類介面在繼承時的情形相反。(注:此處應該貼出程式碼…)

異常匹配

丟擲異常的時候,異常處理系統會按照程式碼的書寫順序找出“最近”的處理程式。找到匹配的處理程式之後,它就認為異常將得到處理,然後就不再繼續査找。

査找的時候井不要求丟擲的異常同處理程式所宣告的異常完全匹配。派生類的物件也可以匹配其基類的處理程式,就像這樣:

class Aa extends Exception{}
class Bb extends Aa{}

public class Sneeze {
	public static void main(String[] args) {
		try {
			throw new Bb();
		} catch (Bb b) {
			System.out.println("B");
		} catch (Aa a) {
			System.out.println("A");
		}
		try {
			throw new Bb();
		} catch (Aa a) {
			System.out.println("A");
		}
	}
	//輸出:
	//B
	//A
}

Bb異常會被第一個匹配的catch子句捕獲,也就是程式裡的第一個。然而如果將這個catch子句刪掉,只留下Aa的catch子句,該程式仍然能執行,因為這次捕獲的是Aa的基類。換句話說,catch(Aa)會捕獲Aa以及所有從它派生的異常。這一點非常有用,因為如果決定在方法里加上更多派生異常的話,只要客戶程式設計師捕獲的是基類異常,那麼它們的程式碼就無需更改。

如果把捕獲基類的catch子句放在最前面,以此想把派生類的異常全給“遮蔽”掉,就像這樣: try{     throw new Aa(); }catch(Aa a){     //… }catch(Bb b){     //… } 這樣編譯器就會發現Bb 的一catch子句永遠也得不到執行,因此它會向你報告錯誤。

其他可選方式

異常處理系統就像一個活門(trap door),使你能放棄程式的正常執行序列。當“異常情形發生的時候,正常的執行已變得不可能或者不需要了,這時就要用到這個“活門”。異常代表了當前方法不能繼續執行的情形。開發異常處理系統的原因是,如果為每個方法所有可能發生的錯誤都進行處理的話,任務就顯得過於繁重了,程式設計師也不願意這麼做。結果常常是將錯誤忽略。應該注意到,開發異常處理的初衷是為了方便程式設計師處理錯誤。

異常處理的一個重要原則是“只有在你知道如何處理的情況下才捕獲異常。實際上,異常處理的一個重要目標就是把錯誤處理的程式碼同錯誤發生的地點相分高。這使你能在一段程式碼中專注於要完成的事情,至於如何處理錯誤,則放在另一段程式碼中完成。這樣以來,主幹程式碼就不會與錯誤處理邏輯混在一起,也更容易理解和維護。通過允許一個處理程式去處理多個出錯點,異常處理還使得錯誤處理程式碼的數量趨向於減少。

“被檢査的異常”使這個問題變得有些複雜,因為它們強制你在可能還沒準備好處理錯誤的時候被追加上catch子句,這就導致了吞食則有害(harmful swallowed)的問題:

try{     //…to do something useful }catch(ObligatoryException e){}

程式設計師們只做最簡單的事情,常常是無意中“吞食”了異常,然而一旦這麼做,雖然能通過編譯,但除非你記得複查並改正程式碼,否則異常將會丟失。異常確實發生了,但“吞食”後它卻完全消失了。因為編譯器強制你立刻寫程式碼來處理異常,所以這種看起來最簡單的方法,卻可能是最糟糕的做法。

這個話題看起來簡單,但實際上它不僅複雜,更重要的是還非常多變。總有人會頑固地堅持自己的立場,聲稱正確答案(也是他們的答案)是顯而易見的。我覺得之所以會有這種觀點,是因為我們使用的工具已經不是ANSI標準出臺前的像C那樣的弱型別語言,而是像C++和Java這樣的“強靜態型別語言”(也就是編譯時就做型別檢査的語言),這是前者所無法比擬的。當剛開始這種轉變的時候,會覺得它帶來的好處是那樣明顯,好像型別檢査總能解決所有的問題。在此,我想結合我自已的認識過程,告訴讀者我是怎樣從對型別檢査的絕對迷信變成持懷疑態度的,當然,很多時候它還是非常有用的,但是當它擋住我們的去路併成為障礙的時候,我們就得跨過去。只是這條界限往往並不是很清晰(我(作者)最喜歡的一句格言是:所有模型都是錯誤的,但有些是能用的)。

把“被檢查的異常”轉換為“不檢查的異常”

當在一個普通方法裡呼叫別的方法時,要考慮到“我不知道該這樣處理這個異常,但是也不想把它‘吞’了,或者列印一些無用的訊息”。JDK 1.4的異常鏈提供了一種新的思路來解決這個問題。可以直接把“被檢査的異常”包裝進RuntimeException裡面,就像這樣:

try{     //…to do something useful }catch(IDontKnowWahtToDoWithThisCheckException e){     throw new RuntimeException(e); }

如果想把“被檢査的異常”這種功能“遮蔽”掉的話,這看上去像是一個好辦法。不用“吞下”異常,也不必把它放到方法的異常說明裡面,而異常鏈還能保證你不會丟失任何原始異常的資訊 。 這種技巧給了你一種選擇,你可以不寫try-catch子句和/或異常說明,直接忽略異常,讓它自己沿著呼叫棧往上“冒泡”。同時,還可以用getCause()捕獲並處理特定的異常,就像這樣:

class WrapCheckedExcetion{
	void f(){
		try {
			throw new IOException();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

public class Sneeze {
	public static void main(String[] args) {
		WrapCheckedExcetion w = new WrapCheckedExcetion();
		try {
			w.f();
		} catch (Exception e) {
			try {
				throw e.getCause
            
           

相關推薦

《Java編程思想》筆記 第十二章 通過異常處理錯誤

nts 無法 ble 多個 打印 while循環 sage 返回 機制 1.異常也是對象 標準異常類都有兩個構造器,一個默認,一個接受字符串。 1.1 拋異常與方法返回類型不同,但有相似效果使當前方法退出並返回,拋異常可以看作是一種不同的返回機制。(異同點不必深究)

【Java程式設計思想】12.通過異常處理錯誤

Java 的基本理念是“結構不佳的程式碼不能執行”。 異常處理是 Java 中唯一正式的錯誤報告機制,並且通過編譯器強制執行。 12.1 概念 異常機制會保證能夠捕獲錯誤,並且只需在一個地方(即異常處理程式中)處理錯即可。 12.2 基本異常 異常情形(exceptional conditio

《java程式設計思想——第十二章(通過異常處理錯誤)》

12.1 概念## 發現錯誤的理想時機是編譯時期,然而,編譯期間並不能找出所有的錯誤,餘下的問題必須在執行時期解決。 12.2 基本異常## 異常是指阻止當前方法或作用域繼續執行的問題。 當丟擲異常後,首先在堆上建立異常物件,當前的執行路徑被終止,並從當前環境中彈

java程式設計思想-12通過異常處理錯誤

java的基本理念是“結構不佳的程式碼不能執行”。 1.概念 “異常”這個詞有“我對此感到意外”的意思。問題出現了,你也許不清楚該如何處理,但你的確知道不應該置之不理;你要停下來,看看是不是有別人或在別的地方,能夠處理這個問題。只是在當前的環境中還沒有足夠的資訊來解決這個問題,所以就

通過異常處理錯誤

概念 Java的基本理念是“結構不佳的程式碼不能執行” 如果在每次呼叫方法的時候都徹底地進行錯誤檢査,程式碼很可能會變得難以閱讀。對於構造大型、健壯、可維護的程式而言,這種錯誤處理模式將會成為主要障礙。 我們的做法應該是用強制規定的形式來消除錯誤處理過程中隨心

Java程式設計思想 第十二章:通過異常處理錯誤

發現錯誤的理想時機是在編譯階段,也就是程式在編碼過程中發現錯誤,然而一些業務邏輯錯誤,編譯器並不能一定會找到錯誤,餘下的問題需要在程式執行期間解決,這就需要發生錯誤的地方能夠準確的將錯誤資訊傳遞給某個接收者,以便接收者知道如何正確的處理這個錯誤資訊。 改進錯誤的機制在Java中尤為重要,

java程式設計思想 第十二章 通過異常處理錯誤

1.異常處理是java中唯一正式的錯誤報告機制,並且通過編譯器強制執行 2.異常情形:指阻止當前方法或作用域繼續執行的問題。能做的就是從當前的環境中跳出,把問題提交給上一級環境,這就是丟擲異常時所發生的事情。 3.丟擲異常的時候: 1. 同java

Java程式設計思想第四版第十二章學習——通過異常處理錯誤(1)

使用異常帶來的好處: 它降低了錯誤處理程式碼的複雜度。使用異常後,不需要檢查特定的錯誤並在程式中的許多地方去處理它。因為異常機制將保證能夠捕獲這個錯誤且只需在一個地方處理錯誤,即異常處理程式中。 1、基本異常 異常情形:阻止當前方法或作用域繼續執行的問

通過異常處理錯誤-1

Java的基本理念是“ 結構不佳的程式碼不能執行”。         發現錯誤的理想時機是在編譯階段,也就是在你試圖執行程式之前。然而,編譯期間並不能找出所有的錯誤,餘下的問題必須在執行期間解決。這就需要錯誤源能通過某種方式,把適

Java程式設計思想第四版讀書筆記——第十二章 通過異常處理錯誤

第十二章 通過異常處理錯誤 Java的基本理念是“結構不佳的程式碼不能執行”。 Java中異常處理的目的在於通過使用少於目前數量的程式碼來簡化大型、可靠的程式的生成,並且通過這種方式可以使程式設計師增加自信。 1、概念 因為異常機制將保證能夠捕獲這個錯誤,所以不用小心翼翼

Java程式設計思想之通過異常處理錯誤

1.     異常分為被檢查的異常和執行時異常,被檢查的異常在編譯時被強制要求檢查。異常被用來錯誤報告和錯誤恢復,但很大一部分都是用作錯誤報告的。 2.     異常情形是由於當前環境下無法得到必要的資訊導致當前方法或作用域無法繼續執行。當丟擲異常時,首先在堆上建立了異常物

異常處理錯誤拋出機制

異常處理錯誤拋出機制: 把可能出現異常的代碼寫在try{}裏,使用catch(){}設置一些異常陷阱來捕獲異常。例如:沒有異常處理時異常的拋出機制: 為什麽出現異常會在控制臺上顯示打印紅色的異常呢?這是因為其實main方法外面還有一個try catch,try包圍住main方法,catch捕捉異常,所以在

《java程式設計思想》 第十二章異常處理錯誤

12.4 之前程式裡寫日誌不清楚怎麼把printStackTrace()輸出的內容寫到日誌裡,僅僅是寫getMessage()資訊少了不少。在本節的例子中給出了一個方法: StringWriter sw = new StringWriter(); PrintWriter pw = new

Laravel 5.5 異常處理 & 錯誤日誌

簡介 Laravel 預設已經為我們配置好了錯誤和異常處理,我們在 App\Exceptions\Handler 類中觸發異常並將響應返回給使用者。 此外,Laravel 還集成了 Monolog 日誌庫以便提供各種功能強大的日誌處理器,預設情況下,Laravel 已經為

Oracle中游標詳細用法 隱式遊標 顯式遊標 異常處理 錯誤處理

  oracle中游標詳細用法 轉自:http://blog.csdn.net/liyong199012/article/details/8948952 遊標的概念:      遊標是SQL的一個記憶體工作區,由系統或使用者以變數的形式定義。遊

js 異常處理 錯誤不彈出視窗

問題:幾乎開啟的每個網頁左下角都顯示“網頁有錯誤” 行: 2 char: 1 錯誤: 語法錯誤 程式碼: 0 “腳 本錯誤”形成的原因是因為訪問者所使用的瀏覽器不能完全支援頁面裡的指令碼,而且出現頻率並不低。遇到“指令碼錯誤”時一般會彈出一個非常難看的指令碼執行錯誤 警告視窗,而事實上,指令碼錯誤並不會影響網

Laravel之加密解密/日誌/異常處理及自定義錯誤

文件中 例如 tom 處理器 crypt return cat 情況 而不是 一.加密解密 1.加密Crypt::encrypt($request->secret) 2.解密try {   $decrypted = Crypt::decrypt($encryptedV

Servlet 異常處理( 配置錯誤頁面)

使用 程序 頁面 sco class exception clas type load 當一個 Servlet 拋出一個異常時,Web 容器在使用了 exception-type 元素的 web.xml 中搜索與拋出異常類型相匹配的配置。 您必須在 web.xml 中使用

小議C#錯誤調試和異常處理

才幹 avi blank {} sni 沒有 ng- fill back 在程序設計中不可避免地會出現各種各樣的錯誤,在編寫代碼時須要盡量避免。在處理錯誤時,首先應該分析錯 誤的類型,找出出錯的原因才幹解決錯誤。 錯誤的分類

PHP之 錯誤異常處理

函數 用戶 exceptio 產生 存放位置 如果 date error_log reporting PHP的錯誤報告有三種: 1.錯誤,語法解析錯誤,致命錯誤2.警告3.註意 錯誤 -> 致命錯誤,會終止已下程序的執行,語法錯誤的話,PHP壓根就沒執行警告 ->