?? chapter9.htm
字號:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>Thinking in Java | Chinese Version by Trans Bot</title>
<meta name="Microsoft Theme" content="inmotion 111, default"></head>
<body background="../_themes/inmotion/inmtextb.gif" tppabs="http://member.netease.com/%7etransbot/Thinking%20in%20Java/_themes/inmotion/inmtextb.gif" bgcolor="#FFFFCC" text="#000000" link="#800000" vlink="#996633" alink="#FF3399">
<p>第9章 違例差錯控制<br>
<br>
Java的基本原理就是“形式錯誤的代碼不會運行”。<br>
與C++類似,捕獲錯誤最理想的是在編譯期間,最好在試圖運行程序以前。然而,并非所有錯誤都能在編譯期間偵測到。有些問題必須在運行期間解決,讓錯誤的締結者通過一些手續向接收者傳遞一些適當的信息,使其知道該如何正確地處理遇到的問題。<br>
在C++和其他早期語言中,可通過幾種手續來達到這個目的。而且它們通常是作為一種規定建立起來的,而非作為程序設計語言的一部分。典型地,我們需要返回一個值或設置一個標志(位),接收者會檢查這些值或標志,判斷具體發生了什么事情。然而,隨著時間的流逝,終于發現這種做法會助長那些使用一個庫的程序員的麻痹情緒。他們往往會這樣想:“是的,錯誤可能會在其他人的代碼中出現,但不會在我的代碼中”。這樣的后果便是他們一般不檢查是否出現了錯誤(有時出錯條件確實顯得太愚蠢,不值得檢驗;注釋①)。另一方面,若每次調用一個方法時都進行全面、細致的錯誤檢查,那么代碼的可讀性也可能大幅度降低。由于程序員可能仍然在用這些語言維護自己的系統,所以他們應該對此有著深刻的體會:若按這種方式控制錯誤,那么在創建大型、健壯、易于維護的程序時,肯定會遇到不小的阻撓。<br>
<br>
①:C程序員研究一下printf()的返回值便知端詳。<br>
<br>
解決的方法是在錯誤控制中排除所有偶然性,強制格式的正確。這種方法實際已有很長的歷史,因為早在60年代便在操作系統里采用了“違例控制”手段;甚至可以追溯到BASIC語言的on
error goto語句。但C++的違例控制建立在Ada的基礎上,而Java又主要建立在C++的基礎上(盡管它看起來更象Object
Pascal)。<br>
“違例”(Exception)這個詞表達的是一種“例外”情況,亦即正常情況之外的一種“異常”。在問題發生的時候,我們可能不知具體該如何解決,但肯定知道已不能不顧一切地繼續下去。此時,必須堅決地停下來,并由某人、某地指出發生了什么事情,以及該采取何種對策。但為了真正解決問題,當地可能并沒有足夠多的信息。因此,我們需要將其移交給更級的負責人,令其作出正確的決定(類似一個命令鏈)。<br>
違例機制的另一項好處就是能夠簡化錯誤控制代碼。我們再也不用檢查一個特定的錯誤,然后在程序的多處地方對其進行控制。此外,也不需要在方法調用的時候檢查錯誤(因為保證有人能捕獲這里的錯誤)。我們只需要在一個地方處理問題:“違例控制模塊”或者“違例控制器”。這樣可有效減少代碼量,并將那些用于描述具體操作的代碼與專門糾正錯誤的代碼分隔開。一般情況下,用于讀取、寫入以及調試的代碼會變得更富有條理。<br>
由于違例控制是由Java編譯器強行實施的,所以毋需深入學習違例控制,便可正確使用本書編寫的大量例子。本章向大家介紹了用于正確控制違例所需的代碼,以及在某個方法遇到麻煩的時候,該如何生成自己的違例。<br>
<br>
9.1 基本違例<br>
“違例條件”表示在出現什么問題的時候應中止方法或作用域的繼續。為了將違例條件與普通問題區分開,違例條件是非常重要的一個因素。在普通問題的情況下,我們在當地已擁有足夠的信息,可在某種程度上解決碰到的問題。而在違例條件的情況下,卻無法繼續下去,因為當地沒有提供解決問題所需的足夠多的信息。此時,我們能做的唯一事情就是跳出當地環境,將那個問題委托給一個更高級的負責人。這便是出現違例時出現的情況。<br>
一個簡單的例子是“除法”。如可能被零除,就有必要進行檢查,確保程序不會冒進,并在那種情況下執行除法。但具體通過什么知道分母是零呢?在那個特定的方法里,在我們試圖解決的那個問題的環境中,我們或許知道該如何對待一個零分母。但假如它是一個沒有預料到的值,就不能對其進行處理,所以必須產生一個違例,而非不顧一切地繼續執行下去。<br>
產生一個違例時,會發生幾件事情。首先,按照與創建Java對象一樣的方法創建違例對象:在內存“堆”里,使用new來創建。隨后,停止當前執行路徑(記住不可沿這條路徑繼續下去),然后從當前的環境中釋放出違例對象的句柄。此時,違例控制機制會接管一切,并開始查找一個恰當的地方,用于繼續程序的執行。這個恰當的地方便是“違例控制器”,它的職責是從問題中恢復,使程序要么嘗試另一條執行路徑,要么簡單地繼續。<br>
作為產生違例的一個簡單示例,大家可思考一個名為t的對象句柄。有些時候,程序可能傳遞一個尚未初始化的句柄。所以在用那個對象句柄調用一個方法之前,最好進行一番檢查。可將與錯誤有關的信息發送到一個更大的場景中,方法是創建一個特殊的對象,用它代表我們的信息,并將其“擲”(Throw)出我們當前的場景之外。這就叫作“產生一個違例”或者“擲出一個違例”。下面是它的大概形式:<br>
if(t == null)<br>
throw new NullPointerException();<br>
這樣便“擲”出了一個違例。在當前場景中,它使我們能放棄進一步解決該問題的企圖。該問題會被轉移到其他更恰當的地方解決。準確地說,那個地方不久就會顯露出來。<br>
<br>
9.1.1 違例自變量<br>
和Java的其他任何對象一樣,需要用new在內存堆里創建違例,并需調用一個構建器。在所有標準違例中,存在著兩個構建器:第一個是默認構建器,第二個則需使用一個字串自變量,使我們能在違例里置入相關信息:<br>
if(t == null)<br>
throw new NullPointerException("t = null");<br>
稍后,字串可用各種方法提取出來,就象稍后會展示的那樣。<br>
在這兒,關鍵字throw會象變戲法一樣做出一系列不可思議的事情。它首先執行new表達式,創建一個不在程序常規執行范圍之內的對象。而且理所當然,會為那個對象調用構建器。隨后,對象實際會從方法中返回——盡管對象的類型通常并不是方法設計為返回的類型。為深入理解違例控制,可將其想象成另一種返回機制——但是不要在這個問題上深究,否則會遇到麻煩。通過“擲”出一個違例,亦可從原來的作用域中退出。但是會先返回一個值,再退出方法或作用域。<br>
但是,與普通方法返回的相似性到此便全部結束了,因為我們返回的地方與從普通方法調用中返回的地方是迥然有異的(我們結束于一個恰當的違例控制器,它距離違例“擲”出的地方可能相當遙遠——在調用堆棧中要低上許多級)。<br>
此外,我們可根據需要擲出任何類型的“可擲”對象。典型情況下,我們要為每種不同類型的錯誤“擲”出一類不同的違例。我們的思路是在違例對象以及挑選的違例對象類型中保存信息,所以在更大場景中的某個人可知道如何對待我們的違例(通常,唯一的信息是違例對象的類型,而違例對象中保存的沒什么意義)。<br>
<br>
9.2 違例的捕獲<br>
若某個方法產生一個違例,必須保證該違例能被捕獲,并獲得正確對待。對于Java的違例控制機制,它的一個好處就是允許我們在一個地方將精力集中在要解決的問題上,然后在另一個地方對待來自那個代碼內部的錯誤。<br>
為理解違例是如何捕獲的,首先必須掌握“警戒區”的概念。它代表一個特殊的代碼區域,有可能產生違例,并在后面跟隨用于控制那些違例的代碼。<br>
<br>
9.2.1 try塊<br>
若位于一個方法內部,并“擲”出一個違例(或在這個方法內部調用的另一個方法產生了違例),那個方法就會在違例產生過程中退出。若不想一個throw離開方法,可在那個方法內部設置一個特殊的代碼塊,用它捕獲違例。這就叫作“try塊”,因為要在這個地方“嘗試”各種方法調用。try塊屬于一種普通的作用域,用一個try關鍵字開頭:<br>
<br>
try {<br>
// 可能產生違例的代碼<br>
}<br>
<br>
若用一種不支持違例控制的編程語言全面檢查錯誤,必須用設置和錯誤檢測代碼將每個方法都包圍起來——即便多次調用相同的方法。而在使用了違例控制技術后,可將所有東西都置入一個try塊內,在同一地點捕獲所有違例。這樣便可極大簡化我們的代碼,并使其更易辨讀,因為代碼本身要達到的目標再也不會與繁復的錯誤檢查混淆。<br>
<br>
9.2.2 違例控制器<br>
當然,生成的違例必須在某個地方中止。這個“地方”便是違例控制器或者違例控制模塊。而且針對想捕獲的每種違例類型,都必須有一個相應的違例控制器。違例控制器緊接在try塊后面,且用catch(捕獲)關鍵字標記。如下所示:<br>
<br>
407-408頁程序<br>
<br>
每個catch從句——即違例控制器——都類似一個小型方法,它需要采用一個(而且只有一個)特定類型的自變量。可在控制器內部使用標識符(id1,id2等等),就象一個普通的方法自變量那樣。我們有時也根本不使用標識符,因為違例類型已提供了足夠的信息,可有效處理違例。但即使不用,標識符也必須就位。<br>
控制器必須“緊接”在try塊后面。若“擲”出一個違例,違例控制機制就會搜尋自變量與違例類型相符的第一個控制器。隨后,它會進入那個catch從句,并認為違例已得到控制(一旦catch從句結束,對控制器的搜索也會停止)。只有相符的catch從句才會得到執行;它與switch語句不同,后者在每個case后都需要一個break命令,防止誤執行其他語句。<br>
在try塊內部,請注意大量不同的方法調用可能生成相同的違例,但只需要一個控制器。<br>
<br>
1. 中斷與恢復<br>
在違例控制理論中,共存在兩種基本方法。在“中斷”方法中(Java和C++提供了對這種方法的支持),我們假定錯誤非常關鍵,沒有辦法返回違例發生的地方。無論誰只要“擲”出一個違例,就表明沒有辦法補救錯誤,而且也不希望再回來。<br>
另一種方法叫作“恢復”。它意味著違例控制器有責任來糾正當前的狀況,然后取得出錯的方法,假定下一次會成功執行。若使用恢復,意味著在違例得到控制以后仍然想繼續執行。在這種情況下,我們的違例更象一個方法調用——我們用它在Java中設置各種各樣特殊的環境,產生類似于“恢復”的行為(換言之,此時不是“擲”出一個違例,而是調用一個用于解決問題的方法)。另外,也可以將自己的try塊置入一個while循環里,用它不斷進入try塊,直到結果滿意時為止。<br>
從歷史的角度看,若程序員使用的操作系統支持可恢復的違例控制,最終都會用到類似于中斷的代碼,并跳過恢復進程。所以盡管“恢復”表面上十分不錯,但在實際應用中卻顯得困難重重。其中決定性的原因可能是:我們的控制模塊必須隨時留意是否產生了違例,以及是否包含了由產生位置專用的代碼。這便使代碼很難編寫和維護——大型系統尤其如此,因為違例可能在多個位置產生。<br>
<br>
9.2.3 違例規范<br>
在Java中,對那些要調用方法的客戶程序員,我們要通知他們可能從自己的方法里“擲”出違例。這是一種有禮貌的做法,只有它才能使客戶程序員準確地知道要編寫什么代碼來捕獲所有潛在的違例。當然,若你同時提供了源碼,客戶程序員甚至能全盤檢查代碼,找出相應的throw語句。但盡管如此,通常并不隨同源碼提供庫。為解決這個問題,Java提供了一種特殊的語法格式(并強迫我們采用),以便禮貌地告訴客戶程序員該方法會“擲”出什么違例,令對方方便地加以控制。這便是我們在這里要講述的“違例規范”,它屬于方法聲明的一部分,位于自變量(參數)列表的后面。<br>
違例規范采用了一個額外的關鍵字:throws;后面跟隨全部潛在的違例類型。因此,我們的方法定義看起來應象下面這個樣子:<br>
void f() throws tooBig, tooSmall, divZero { //...<br>
若使用下述代碼:<br>
void f() [ // ...<br>
它意味著不會從方法里“擲”出違例(除類型為RuntimeException的違例以外,它可能從任何地方擲出——稍后還會詳細講述)。<br>
但不能完全依賴違例規范——假若方法造成了一個違例,但沒有對其進行控制,編譯器會偵測到這個情況,并告訴我們必須控制違例,或者指出應該從方法里“擲”出一個違例規范。通過堅持從頂部到底部排列違例規范,Java可在編譯期保證違例的正確性(注釋②)。<br>
<br>
②:這是在C++違例控制基礎上一個顯著的進步,后者除非到運行期,否則不會捕獲不符合違例規范的錯誤。這使得C++的違例控制機制顯得用處不大。<br>
<br>
我們在這個地方可采取欺騙手段:要求“擲”出一個并沒有發生的違例。編譯器能理解我們的要求,并強迫使用這個方法的用戶當作真的產生了那個違例處理。在實際應用中,可將其作為那個違例的一個“占位符”使用。這樣一來,以后可以方便地產生實際的違例,毋需修改現有的代碼。<br>
<br>
9.2.4 捕獲所有違例<br>
我們可創建一個控制器,令其捕獲所有類型的違例。具體的做法是捕獲基礎類違例類型Exception(也存在其他類型的基礎違例,但Exception是適用于幾乎所有編程活動的基礎)。如下所示:<br>
catch(Exception e) {<br>
System.out.println("caught an exception");<br>
}<br>
這段代碼能捕獲任何違例,所以在實際使用時最好將其置于控制器列表的末尾,防止跟隨在后面的任何特殊違例控制器失效。<br>
對于程序員常用的所有違例類來說,由于Exception類是它們的基礎,所以我們不會獲得關于違例太多的信息,但可調用來自它的基礎類Throwable的方法:<br>
<br>
String getMessage()<br>
獲得詳細的消息。<br>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -