引言
在程序運行過程中(注意是運行階段,程序可以通過編譯),如果JVM檢測出一個不可能執行的操作,就會出現運行時錯誤。例如,使用一個越界的下標訪問數組,程序就會產生一個ArrayIndexOutOfBoundsException的運行時錯誤。如果程序需要輸入一個整數的時候用戶輸入了一個double值,會得到一個InputMismatchException的運行時錯誤。
在Java中,運行時錯誤會作為異常拋出。異常就是一種對象,表示阻止正常進行程序執行的錯誤或者情況。如果異常沒有被處理,那么程序就會非正常終止。
人們在遇到錯誤時會感覺不爽。如果一個用戶在運行程序期間,由于程序的錯誤或一些外部環境的影響造成用戶數據的丟失,用戶就有可能不再使用這個程序了,為了避免這類事情的發生,至少應該做到以下幾點:
- 向用戶通告錯誤
- 保存所有的工作結果
- 允許用戶以妥善的形式退出程序
Java使用一種稱為異常處理的錯誤捕獲機制處理,從而使程序繼續運行或優雅終止。
異常處理概述
異常處理使得程序可以處理非預期的情景,并且繼續正常的處理。
我們來看一個讀取兩個整數并顯示它們商的例子:
public class Quotient{
public static void main(String[] args){
Scanner input = new Scanner(System.in);
System.out.print("Enter two integers: ");
int number1 = input.nextInt();
int number2 = input.nextInt();
System.out.println(number1 + " / " + number2
+ " is " + (number1 / number2));
}
}
如果number2為0,就會產生一個運行時錯誤,因為不能用一個整數除以0(注意,一個浮點數除以0不會產生異常)。
我們可以添加一個if語句來測試第二個數據:
public class Quotient{
public static void main(String[] args){
Scanner input = new Scanner(System.in);
System.out.print("Enter two integers: ");
int number1 = input.nextInt();
int number2 = input.nextInt();
if(number2 != 0)
System.out.println(number1 + " / " + number2
+ " is " + (number1 / number2));
else
System.out.println("Divisor cannot be zero");
}
}
為了介紹異常處理,我們使用一個方法來實現兩個整數求商的操作:
public class QuotientWithMethod {
public static int quotient(int number1,int number2) {
if(number2 == 0) {
System.out.println("Divisor cannot be zero");
System.exit(0);
}
return number1 / number2;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("Enter two integers: ");
int number1 = input.nextInt();
int number2 = input.nextInt();
int result = quotient(number1,number2);
System.out.println(number1 + " / " + number2
+ " is " + result);
但上述代碼有一個問題:當number2為0時,程序在quotient方法內終止。但不應該讓一個方法來終止程序 —— 應該由方法的調用者決定是否終止程序,即方法只需要通知其調用者有運行時錯誤產生,而不應該自己做決定。
下面使用異常處理的方法,讓quotient方法拋出一個異常,使其被調用這捕獲和處理:
public class QuotientWithException {
public static int quotient(int number1,int number2) {
if (number2 == 0)
throw new ArithmeticException("Divisor cannot be zero");
return number1 / number2;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("Enter two integers: ");
int number1 = input.nextInt();
int number2 = input.nextInt();
try{
int result = quotient(number1,number2);
System.out.println(number1 + " / " + number2 + " is "
+ result);
}
catch(ArithmeticException ex) {
System.out.println("Exception: an integer " +
"cannot be divided by zero");
}
System.out.println("Execution continues ...");
}
}
我們可以看到,上面的代碼能使方法拋出一個異常給調用者,并由調用者處理該異常。如果不這么做,被調用的方法本身必須處理異常或者終止程序。但是庫方法在設計時通常無法確定在出錯時要進行什么操作,最好的做法就是將檢測出的錯誤作為異常拋出給調用者處理,查閱API我們也會發現庫方法會對其可能拋出的異常進行說明。異常處理的最根本優勢就是將檢測錯誤(由被調用的方法完成)從處理錯誤(由調用方法完成)中分離出來。
當然,如果運行時錯誤發生在main方法中,就不必拋出異常了,可以考慮提供一個異常處理器對異常進行捕獲和處理。
異常類型
異常是對象,而對象都采用類來定義。在 Java 程序設計語言中, 異常對象都是派生于 Throwable 類的一個實例。稍后還可以看到,如果 Java 中內置的異常類不能夠滿足需求,用戶可以創建自己的異常類。
下面是Java中的異常層次結構:
可以看到,Throwable是所有異常類的根類,所有異常類都直接或間接繼承自 Throwable。但在下一層立即分解為兩個分支:Error 和 Exception。
Error 類層次結構描述了 Java 運行時JVM的內部錯誤和資源耗盡錯誤。比如 OutOfMemoryError 和 StackOverFlowError。應用程序不應該拋出這種類型的對象。 如果出現了這樣的內部錯誤, 除了通告給用戶,并盡力使程序安全地終止之外, 再也無能為力了。這種情況很少出現。
在設計 Java 程序時, 需要關注 Exception 層次結構。 這個層次結構又分解為兩個分支:
一個分支派生于 RuntimeException ; 另一個分支包含其他異常。劃分兩個分支的規則是:由程序錯誤導致的異常屬于RuntimeException;而程序本身沒有問題,但由于像 I/O 錯誤這類問題導致的異常屬于其他異常。
有一條相當有道理的規則:如果出現 RuntimeException,那么就一定是你的問題。
也就是說,RuntimeException是可以在編程時避免的。比如,可以通過檢測數組下標是否越界來避免IndexOutOfBoundsException,可以通過在使用變量前檢測是否為null杜絕NullPointerException。
其實,還可以使用Optional類來優雅地避免空指針:Optional 是個好東西,你真的會用么?
免檢異常:又稱非受查異常(Unchecked Exception),RuntimeException、Error以及它們的子類都稱為免檢異常。意思是編譯器不會強制檢查程序是否處理或聲明了異常。如果想讓使用某方法的程序員注意到方法可能拋出的免檢異常,可以給該方法加上文檔注釋。
必檢異常:又稱受查異常(Checked Exception),除了免檢異常的其他異常都是必檢異常,意思是編譯器會強制程序員檢查并通過try-catch語句處理它們,或者在方法頭進行聲明,否則無法通過編譯。
關于異常處理的更多知識
異常處理器是通過從當前的方法開始,沿著方法調用鏈,按照異常的反向傳播方向找到的。即如果某方法的異常沒有在該方法內被捕獲和處理,就會被拋出給它的調用者,并在調用者中搜尋相應的異常處理器,如果還沒有找到就繼續上拋,如果在整個方法調用鏈中異常都沒有被捕獲處理,該異常會被拋給JVM,JVM會終止程序并打印錯誤信息。
Java的異常處理模型基于三種操作:
- 聲明異常
- 拋出異常
- 捕獲異常
聲明異常
一個方法不僅需要告訴編譯器將要返回什么值,還要告訴編譯器有可能發生什么錯誤。例如,一段讀取文件的代碼知道有可能讀取的文件不存在, 或者內容為空,因此, 試圖處理文件信息的代碼就需要通知編譯器可能會拋出 IOException 類的異常。
方法應該在其首部聲明所有可能拋出的異常,這樣可以從首部反映出這個方法可能拋出異常。
每個方法只需聲明所有它可能拋出的必檢異常類型,這稱為聲明異常。無需聲明免檢異常,因為免檢異常要么不可控制(Error),要么就應該避免發生(RuntimeException)。
可以聲明多個異常,用逗號隔開即可:
public void myMethod() throws Exception1, Exception2,...
當然,從前面的示例中可以知道:除了聲明異常之外, 還可以捕獲異常。這樣會使異常不被拋到方法之外,也不需要 throws 規范。稍后,將會討論如何決定一個異常是被捕獲,還是被拋出讓其他的處理器進行處理。
下面有一些規則:
- 在方法定義處聲明的異常類型可以是方法內拋出異常的類型及其父類型。
- 如果在子類中重寫了父類的一個方法,子類方法中聲明的受查異常必須是父類所聲明異常的同類或子類(也就是說,子類方法中可以拋出更特定的異常,或者根本不拋出任何異常)
- 如果在超類方法中沒有聲明/拋出異常,子類也不能聲明/拋出異常
拋出異常
檢測到錯誤的程序可以創建一個合適的異常類型的實例并拋出它,這就稱為拋出異常。下面有一個例子,方法的參數必須是非負的,如果傳入一個負參數,程序就創建一個IllegalArgumentException實例并拋出它:
IllegalArgumentException ex =
new IllegalArgumentException("Wrong Argument");
throw ex;
或者
throw new IllegalArgumentException("Wrong Argument");
第一種寫法創建了一個異常對象并賦給一個異常類引用變量,并拋出它;第二種寫法則直接拋出一個匿名異常對象。
Java庫中每個異常類一般至少有兩個構造方法:一個無參構造方法和一個帶可描述這個異常的String參數的構造方法。如上述就使用了帶參數的構造方法并傳入了"Wrong Argument"的異常描述。可以通過在異常對象上調用getMessage()獲取異常描述字符串。
拋出異常的三個步驟:
- 找到一個合適的異常類
- 創建這個類的一個對象
- 將對象拋出
注意:這里所說拋出異常是指我們在編寫程序時用throw關鍵字顯式拋出異常,但是在很多情況下,異常是由庫方法拋出的,throw關鍵字被封裝在庫方法中,對用戶是不可見的,此時用戶程序中是沒有顯式的throw關鍵字的。
我們使用throw關鍵字手動拋出異常有兩種基本方案:
1、在throw語句外加上對應異常的try-catch塊,即自己拋出的異常自己捕獲處理。
2、在含有throw語句的方法聲明處通過throws關鍵字聲明對應的異常,由方法的調用者來處理這個異常。
捕獲異常
當拋出一個異常時,可以提供try-catch語句來捕獲和處理它,如下所示:
try {
statements; // Statements that may throw exceptions
}
catch(Exception exVar1) {
handler for exception1;
}
catch(Exception exVar2) {
handler for exception2;
}
...
catch(Exception exVarN) {
handler for exceptionN;
}
可以為一個try塊提供多個catch語句,因為一個try塊可能拋出多種不同類型的異常。
如果在執行try塊的過程中沒有出現異常,則跳過catch子句。
如果try塊中的某條語句拋出一個異常,Java就會跳過try塊中剩余的語句,然后開始查找合適的處理異常的代碼,即異常處理器。可以從當前的方法開始,沿著方法調用鏈,按照異常的反向傳播方向找到這個處理器。從第一個到最后一個逐個檢查catch塊,判斷在catch塊中的異常類變量是否是該異常對象的類型。如果是,就將該異常對象賦值給所聲明的變量,然后執行catch塊中的代碼。如果沒有發現異常處理器,Java會退出這個方法,把異常傳遞給調用這個方法的方法,繼續同樣的過程來查找處理器。如果在調用的方法鏈中找不到處理器,程序就會終止并且在控制臺上打印出錯信息。尋找處理器的過程稱為捕獲異常。
注意:如果一個catch塊可以捕獲一個父類的異常對象,它就能捕獲那個父類的所有子類的異常對象。在catch塊中異常被指定的順序是非常重要的,如果父類異常的catch塊在子類異常的catch塊之前,就會導致編譯錯誤。道理很簡單,如果將父類異常的catch塊放在子類異常的catch塊之前,則子類異常對象一定會被父類異常的catch塊捕獲,子類異常的catch塊就失去了意義。因為我們無法保證所編寫的catch塊涵蓋了try塊中可能出現的所有異常類型,所以建議在多重catch塊的最后添加所有異常的父類Exception的異常處理器來保證try塊中出現的任何異常被捕獲。
對于使用同樣的處理代碼處理多個異常的情況,可以使用多捕獲特征簡化異常的代碼編寫,如:
catch(Exception1 | Exception2 | ... | ExceptionN ex) {
// Same code for handling these exceptions
}
小提示:對于InputMismatchedException,要在catch塊中吸收錯誤輸入,否則該錯誤輸入將被下一條讀取語句讀取。
創建自定義異常類
在程序中,可能會遇到任何標準異常類都沒有能夠充分地描述清楚的問題。在這種情況下,我們可以通過派生Exception類或其子類來創建自定義的異常類。
下面給出一個例子,當半徑為負時,setRadius方法會拋出一個異常:
public class InvalidRadiusException extends Exception {
private double radius;
public InvalidRadiusException(double radius) {
super("Invalid radius " + radius);
this.radius = radius;
}
public double getRadius() {
return radius;
}
可見異常類里可定義數據域和訪問器,使外界能訪問到導致異常的非法參數。
注意:建議不要讓自定義的異常類繼承RuntimeException及其子類,這樣會使自定義的異常類稱為免檢異常,最好使自定義的異常類必檢,這樣編譯器就可以在程序中強制捕獲或聲明這些異常。
從異常中獲取信息
異常對象中包含了關于異常的有價值的信息,可以利用Throwable類中的實例方法獲取有關的信息,如下所示:
- Throwable() 無參構造器
- Throwable(String message) 帶描述異常信息字符串的構造器
- String getMessage() 返回一個描述該異常對象信息的字符串
- String toString() 返回三個字符串的連接:1) 異常類的全名; 2) ": " 一個冒號和一個空格 3) getMessage(方法)
- void printStackTrace() 在控制臺上打印 Throwable對象和它的調用堆棧信息
同樣Exception和RuntimeException也有類似的方法
堆棧軌跡(stack trace)是一個方法調用過程的列表,它包含了程序執行過程中方法調用的特定位置。類似于數據結構中的棧,一個方法被調用就會入棧,即最先被調用的方法(main方法)在棧底,后被調用的方法在棧頂。當一個方法調用結束,就會出棧,也是棧頂方法先出棧,最后main方法也調用完畢,整個方法棧被銷毀,程序結束。
Throwable的printStackTrace方法就是這樣從上到下打印了方法棧,棧頂是產生異常的方法,棧底是main方法。比如下面的代碼訪問了數組的-1下標,拋出一個ArrayIndexOutOfBoundsException:
public class TestException {
public static void main(String[] args) {
int[] array = {1,2,3,4,5};
printArrayElement(array,-1);
}
public static void printArrayElement(int[] a,int index) {
System.out.println(a[index]);
}
}
打印的堆棧軌跡是:
一種更靈活的方法是getStackTrace(),它會得到一個StackTraceElement對象的一個數組,每個元素都是方法堆棧中的一個方法,其API如下:
再次拋出異常與異常鏈
當異常被捕獲之后,可以在catch子句中重新拋出異常,這樣做的目的是改變異常的類型。如果開發了一個供其他程序員使用的子系統,那么,用于表示子系統的異常類型可能會產生多種解釋。ServletException就是這樣一個異常類型的例子。執行servlet的代碼可能不想知道發生錯誤的細節原因,但希望明確地知道servlet是否有問題。
同原始異常一起拋出一個新異常(帶有附加信息),這稱為異常鏈。
下面給出了拋出異常鏈的基本方法:
try
{
access the database
}
catch(SQLException e)
{
Throwable se = new ServletException("database error: "
+ e.getMessage());
}
不過,我們發現原始異常被改變了。有一種更好的處理方法,可以將原始異常設置為新異常的"原因":
try
{
access the database
}
catch(SQLException e)
{
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
當捕獲到異常時,就可以使用下面的這條語句重新得到原始異常:
Throwable e = se.getCause();
強烈建議使用這種包裝技術,這樣可以讓用戶拋出子系統中的高級異常,而不會丟失原始異常的細節。也可以使用帶有包裝功能的構造方法來封裝原始異常并拋出該新異常。
finally子句
當代碼拋出一個異常時,就會終止方法中剩余代碼的處理,并退出這個方法的執行。如果方法獲得了一些本地資源,并且只有這個方法自己知道,又如果這些資源在退出方法之前必須被回收,那么就會產生資源回收問題。一種解決方案是捕獲并重新拋出所有的異常。但是,這種解決方案比較乏味,這是因為需要在兩個地方清除所分配的資源。一個在正常的代碼中;另一個在異常代碼中。
Java 有一種更好的解決方案,這就是 finally 子句。finally子句是可選項,可以有也可以無,但是每個try語句至少需要一個catch或者finally子句。無論異常是否產生,finally子句總是會被執行,即使在到達finally子句之前有一個return語句,finally塊還是會執行。唯一使finally子句不執行的方法是在finally子句前使用System.exit(1)
方法,這個方法的作用是終止正在運行的JVM,參數為0表示程序正常終止,非0表示異常終止。在try塊(或try-catch塊)和finally塊之間不能有其他任何代碼。finally子句常用于在拋出異常時關閉資源,比如關閉文件和關閉與數據庫的連接。
比如下面的代碼:
InputStream in = new FileInputStream(. . .);
try
{
//1
code that might throw exceptions
//2
}
catch (IOException e)
{
// 3
show error message
// 4
}
finally
{
// 5
in.close();
}
//6
在上面的代碼中,有下列3種情況會執行finally子句:
- 代碼沒有拋出異常。在這種情況下,程序首先執行 try 語句塊中的全部代碼,然后執行 finally 子句中的代碼。隨后,繼續執行 try 語句塊之后的下一條語句。也就是說,執行標
注的1、2、5、6處 - 拋出一個在 catch 子句中捕獲的異常。在上面的示例中就是 IOException 異常。在這種情況下,程序將執行 try語句塊中的所有代碼,直到發生異常為止。此時,將跳過 try語句塊中的剩余代碼,轉去執行與該異常匹配的 catch 子句中的代碼, 最后執行 finally 子句中的代碼。
如果 catch 子句沒有拋出異常,程序將執行 try 語句塊之后的第一條語句。在這里,執行標注 1、 3、 4、5、 6 處的語句。
如果 catch 子句拋出了一個異常, 異常將被拋回這個方法的調用者。在這里, 執行標注
1、 3、 5 處的語句。 - 代碼拋出了一個異常,但這個異常不是由 catch 子句捕 獲的。在這種情況下,程序將執行 try 語句塊中的所有語句,直到有異常被拋出為止。此時,將跳過 try 語句塊中的剩余代
碼,然后執行 finally 子句中的語句,并將異常拋給這個方法的調用者。在這里, 執行標注 1、5 處的語句。
try 語句可以只有 finally 子句,而沒有 catch 子句。例如,下面這條 try 語句:
InputStream in = . .
try
{
code that might throw exceptions
}
finally
{
in.close();
}
無論在 try 語句塊中是否遇到異常,finally 子句中的 in.close()語句都會被執行。當然,
如果真的遇到一個異常,這個異常將會被重新拋出,并且必須由另一個 catch 子句捕獲。
強烈建議解耦合 try/catch 和 try/finally 語句塊。這樣可以提高代碼的清晰度。例如:
InputStream in = . . .;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close();
}
}
catch (IOException e)
{
show error message
}
內層的 try 語句塊只有一個職責,就是確保關閉輸入流。外層的 try 語句塊也只有一個職責,就是確保報告出現的錯誤。這種設計方式不僅清楚, 而且還具有一個功能,就是將會報告 finally 子句中出現的錯誤。
注意:當 finally 子句包含 return 語句時,將會出現一種意想不到的結果? 假設利用 return 語句從 try 語句塊中退出。在方法返回前,finally 子句的內容將被執行。如果 finally 子句中也有一個 return 語句,這個返回值將會覆蓋原始的返回值。請看一個復雜的例子:
public static int f(int n)
{
try
{
int r = n * n;
return r;
}
finally
{
if (n == 2) return 0;
}
}
如果調用 f(2), 那么 try 語句塊的計算結果為 r = 4, 并執行 return 語句然而,在方法真正返回前,還要執行 finally 子句。finally 子句將使得方法返回 0, 這個返回值覆蓋了原始的返回值4。
finally子句易錯題: https://www.nowcoder.com/questionTerminal/ebe94f2eae814d30b12464487c53649c
有時候, finally 子句也會帶來麻煩。例如, 清理資源的方法也有可能拋出異常。假設希望能夠確保在流處理代碼中遇到異常時將流關閉。
InputStream in = . . .;
try
{
code that might throw exceptions
}
finally
{
in.close();
}
現在,假設在 try 語句塊中的代碼拋出了一些非 IOException 的異常,這些異常只有這個方法的調用者才能夠給予處理。執行 finally 語句塊,并調用 close 方法。而 close 方法本身也
有可能拋出 IOException 異常。當出現這種情況時, 原始的異常將會丟失,轉而拋出 close 方法的異常。
這會有問題, 因為第一個異常很可能更有意思。如果你想做適當的處理,重新拋出原來的異常, 代碼會變得極其繁瑣。 如下所示:
InputStream in = . . .;
Exception ex = null;
try
{
try
{
code that might throw exceptions
}
catch (Exception e)
{
ex = e;
throw ex;
}
}
finally
{
try
{
in.close();
}
catch (Exception e)
{
if (ex = null) throw e;
}
}
上面的代碼太繁瑣,在 Java SE 7中提供了一種更便捷的方法。
帶資源的try語句
對于以下代碼模式:
open a resource
try
{
work with the resource
}
finally
{
close the resource
}
假設資源屬于一個實現了 AutoCloseable 接口的類,Java SE 7 為這種代碼模式提供了一個很有用的快捷方式。AutoCloseable 接口有一個方法:
void close() throws Exception
另外,還有一個 Closeable 接口。這是 AutoCloseable 的子接口, 也包含一個 close方法。不過,這個方法聲明為拋出一個 IOException。
帶資源的 try 語句(try-with-resources) 的最簡形式為:
try (聲明和創建資源){
使用資源來處理文件;
}
try塊退出時,會自動調用 res.close()。下面給出一個典型的例子, 這里要讀取一個文件中的所有單詞:
try (Scanner in = new Scanner(new FileInputStream(7usr/share/dict/words")), "UTF-8")
{
while (in.hasNext())
System.out.println(in.next());
}
這個塊正常退出時, 或者存在一個異常時, 都會調用 in.close() 方法, 就好像使用了finally塊一樣。
還可以指定多個資源,例如:
try (Scanner in = new Scanne(new FileInputStream("7usr/share/dict/words"), "UTF-8");
PrintWriter out = new PrintWriter("out.txt"))
{
while (in.hasNext())
out.println(in.next().toUpperCase());
}
不論這個塊如何退出, in 和 out 都會關閉。如果你用常規方式手動編程,就需要兩個嵌套的 try/finally語句。
前面已經看到,如果 try 塊拋出一個異常, 而且 close 方法也拋出一個異常,這就會帶來一個難題。帶資源的 try 語句可以很好地處理這種情況。原來的異常會重新拋出,而 close方法拋出的異常會"被抑制"。這些異常將自動捕獲,并由 addSuppressed 方法增加到原來的異常。 如果對這些異常感興趣, 可以調用 getSuppressed 方法,它會得到從 close 方法拋出并被抑制的異常列表。
你肯定不想采用這種常規方式編程。只要需要關閉資源, 就要盡可能使用帶資源的 try語句。
使用異常機制的技巧
1.異常處理不能代替簡單的測試
異常處理需要初始化新的異常對象,需要調用棧返回,而且還需要沿著方法調用鏈來傳播異常以找到它的異常處理器,所以,異常處理通常需要更多的時間和資源。
如果能在發生異常的方法中處理異常,就不需要拋出異常。在個別方法中的簡單錯誤最好進行局部處理,無須拋出異常。
例如:
try {
System.out.println(refVar.toString());
}
catch(NullPointerException ex) {
System.out.println("refVar is null");
}
最好用下面的代碼代替:
if (refVar != null)
System.out.println(refVar.toString());
else
System.out.println("refVar is null");
只有在異常不可預料的情況下才拋出異常,簡單的情況不應該使用異常機制。
2.不要過分細化異常
很多程序員習慣將每一條語句都分裝在一個獨立的 try 語句塊中。
PrintStream out;
Stack s;
for (i = 0;i < 100; i++)
{
try
{
n = s.pop();
}
catch (EmptyStackException e)
{
// stack was empty
}
try
{
out.writelnt(n);
}
catch (IOException e)
{
// problem writing to file
}
}
這種編程方式將導致代碼量的急劇膨脹。首先看一下這段代碼所完成的任務。在這里,希望從棧中彈出 100 個數值, 然后將它們存入一個文件中。如果棧是空的, 則不會變成非空狀態;如果文件出現錯誤, 則也很難給予排除。出現上述問題后,這種編程方式無能為力。因此,有必要將整個任務包裝在一個 try語句塊中,這樣,當任何一個操作出現問題時,整個任務都可以取消。
try
{
for (i = 0; i < 100; i++)
{
n = s.pop();
out.writelnt(n);
}
}
catch (IOException e)
{
// problem writing to file
}
catch (EmptyStackException e)
{
// stack was empty
}
這段代碼看起來清晰多了。這樣也滿足了異常處理機制的其中一個目標,將正常處理與錯誤處理分開。
3.利用異常層次結構
不要只拋出 RuntimeException 異常。應該尋找更加適當的子類或創建自己的異常類。
不要只捕獲 Thowable 異常, 否則,會使程序代碼更難讀、 更難維護。
考慮受查異常與非受查異常的區別。 受查異常本來就很龐大,不要為邏輯錯誤拋出這些異常。(例如, 反射庫的做法就不正確。 調用者卻經常需要捕獲那些早已知道不可能發生的異常。)
將一種異常轉換成另一種更加適合的異常時不要猶豫。例如, 在解析某個文件中的一個整數時,捕獲NumberFormatException 異 常,然后將它轉換成 IOException 或 MySubsystemException 的子類。
4.不要壓制異常
在 Java 中,往往強烈地傾向關閉異常。如果編寫了一個調用另一個方法的方法,而這個方法有可能 100 年才拋出一個異常, 那么, 編譯器會因為沒有將這個異常列在 throws 表中產生抱怨。而沒有將這個異常列在 throws 表中主要出于編譯器將會對所有調用這個方法的方法進行異常處理的考慮。因此,應該將這個異常關閉:
public Image loadImage(String s)
{
try
{
// code that threatens to throw checked exceptions
}
catch (Exception e)
{} // so there
}
現在,這段代碼就可以通過編譯了。除非發生異常,否則它將可以正常地運行。即使發生了異常也會被忽略。如果認為異常非常重要,就應該對它們進行處理。
5.在檢測錯誤時,"苛刻"要比放任更好
當檢測到錯誤的時候,有些程序員擔心拋出異常。在用無效的參數調用一個方法時,返回一個虛擬的數值, 還是拋出一個異常, 哪種處理方式更好? 例如, 當棧空時,Stack.pop 是
返回一個 null, 還是拋出一個異常? 我們認為:在出錯的地方拋出一個 EmptyStackException異常要比在后面拋出一個 NullPointerException 異常更好。
6.不要羞于傳遞異常
很多程序員都感覺應該捕獲拋出的全部異常。如果調用了一個拋出異常的方法,例如,FilelnputStream 構造器或 readLine 方法,這些方法就會本能地捕獲這些可能產生的異常。其實, 傳遞異常要比捕獲這些異常更好:
public void readStuff(String filename) throws IOException
// not a sign of shame!
{
InputStream in = new FilelnputStream(filename);
. . .
}
讓高層次的方法通知用戶發生了錯誤, 或者放棄不成功的命令更加適宜。
規則 5、6 可以歸納為"早拋出,晚捕獲"
使用斷言
在測試期間,需要進行大量的檢測以驗證程序操作的正確性。然而,這些檢測可能非常耗時,在測試完成后也不必保留它們,因此,可以將這些檢測刪掉,并在其他測試需要時將它們粘貼回來,這是一件很乏味的事。
1.斷言的概念
假設確信某個屬性符合要求,并且代碼的執行依賴于這個屬性。例如,需要計算:
double y = Math.sqrt(x);
我們確信,這里的 X 是一個非負數值。原因是:X 是另外一個計算的結果,而這個結果不可能是負值;或者 X 是一個方法的參數,而這個方法要求它的調用者只能提供一個正整數。
然而,還是希望進行檢查,以避免讓“不是一個數”的數值參與計算操作。當然,也可以拋出一個異常:
if (x < 0) throw new IllegalArgumentException("x < 0");
但是這段代碼會一直保留在程序中,即使測試完畢也不會自動地刪除。如果在程序中含有大量的這種檢查,程序運行起來會相當慢。
斷言機制允許在測試期間向代碼中插入一些檢査語句。當代碼發布時,這些插入的檢測語句將會被自動地移走。
Java 語言引人了關鍵字 assert。這個關鍵字有兩種形式:
assert 條件;
和assert 條件:表達式;
這兩種形式都會對條件進行檢測,如果結果為 false, 則在第一種形式中會拋出一個 AssertionError 異常。在第二種形式中,表達式將被傳人 AssertionError 的構造器,并轉換成一個消息字符串,在打印異常信息時會隨之顯示出來。
注意:"表達式"部分的唯一目的是產生一個消息字符串。AssertionError 對象并不存儲表達式的值,因此,不可能在以后得到它。正如 JDK 文檔所描述的那樣:如果使用表達式的值,就會鼓勵程序員試圖從斷言中恢復程序的運行,這不符合斷言機制的初衷。
要想斷言 x 是一個非負數值,只需要簡單地使用下面這條語句:
assert x >= 0;
或者將 x 的實際值傳遞給 AssertionError 對象, 從而可以在后面顯示出來:
assert x >= 0 : x;
2.啟用和禁用斷言
在默認情況下,斷言被禁用。可以在運行程序時用
-enableassertions
或 -ea
選項啟用:
java -enableassertions MyApp
需要注意的是,在啟用或禁用斷言時不必重新編譯程序。啟用或禁用斷言是類加載器(class loader) 的功能。當斷言被禁用時,類加載器將跳過斷言代碼,因此,不會降低程序運行的速度。
也可以在某個類或整個包中使用斷言,例如:
java -ea:MyClass -ea:com.mycompany.mylib... MyApp
這條命令將開啟 MyClass 類以及在 com.mycompany.mylib 包和它的子包中的所有類的斷言。選項 -ea 將開啟默認包中的所有類的斷言。 也可以用選項 -disableassertions
或 -da
禁用某個特定類和包的斷言:
java -ea:... -da:MyClass MyApp
有些類不是由類加載器加載,而是直接由虛擬機加載。可以使用這些開關有選擇地啟用或禁用那些類中的斷言。
然而,啟用和禁用所有斷言的 -ea
和 -da
開關不能應用到那些沒有類加載器的"系統類"上。對于這些系統類來說,需要使用 -enablesystemassertions/-esa
開關啟用斷言。
在程序中也可以控制類加載器的斷言狀態。有關這方面的內容請參看本文末尾的 API 注釋。
還可以在eclipse里開啟斷言,只要Run -> Run Configurations -> Arguments頁簽 -> VM arguments文本框中加上斷言開啟的標志:
-enableassertions 或者-ea 就可以了。
3.使用斷言完成參數檢查
在 Java 語言中,給出了3種處理系統錯誤的機制:
- 拋出一個異常
- 日志
- 使用斷言
什么時候應該選擇使用斷言呢? 請記住下面幾點:
- 斷言失敗是致命的、 不可恢復的錯誤。
- 斷言檢查只用于開發和測階段(這種做法有時候被戲稱為“ 在靠近海岸時穿上救生衣,但在海中央時就把救生衣拋掉吧”)。
因此,不應該使用斷言向程序的其他部分通告發生了可恢復性的錯誤,或者,不應該作為程序向用戶通告問題的手段。斷言只應該用于在測試階段確定程序內部的錯誤位置。
下面看一個十分常見的例子:檢查方法的參數。是否應該使用斷言來檢查非法的下標值或null 引用呢? 要想回答這個問題, 首先閱讀一下這個方法的文檔。假設實現一個排序方法。
/**
Sorts the specified range of the specified array in ascending
numerical order.
The range to be sorted extends from fromlndex, inclusive,
to tolndex, exclusive.
@param a the array to be sorted.
@param fromlndex the index of the first element (inclusive)
to be sorted.
@param tolndex the index of the last element (exclusive) to be
sorted.
?throws IllegalArgumentException if fromlndex > tolndex
?throws ArraylndexOutOfBoundsException if fromlndex < 0 or
tolndex > a.length
*/
static void sort(int[] a, int fromlndex, int tolndex)
文檔指出,如果方法中使用了錯誤的下標值,那么就會拋出一個異常。這是方法與調用者之間約定的處理行為。如果實現這個方法,那就必須要遵守這個約定,并拋出表示下標值有誤的異常。因此,這里使用斷言不太適宜。
是否應該斷言 a 不是 null 呢? 這也不太適宜。當 a 是 null 時,這個方法的文檔沒有指出應該采取什么行動。在這種情況下,調用者可以認為這個方法將會成功地返回,而不會拋出
一個斷言錯誤。
然而,假設對這個方法的約定做一點微小的改動:
@param a the array to be sorted (must not be null)
現在,這個方法的調用者就必須注意:不允許用 null 數組調用這個方法,并在這個方法的開頭使用斷言:assert a != null;
計算機科學家將這種約定稱為前置條件(Precondition)。最初的方法對參數沒有前置條件, 即承諾在任何條件下都能夠給予正確的執行。修訂后的方法有一個前置條件,即 a 非空。如果調用者在調用這個方法時沒有提供滿足這個前置條件的參數, 所有的斷言都會失敗,并且這個方法可以執行它想做的任何操作。事實上,由于可以使用斷言,當方法被非法調用時, 將會出現難以預料的結果。有時候會拋出一個斷言錯誤, 有時候會產生一個 null 指針異常, 這完全取決于類加載器的配置。
4.為文檔假設使用斷言
很多程序員使用注釋說明假設條件。看一下下面的示例:
if (i % 3 == 0)
. . .
else if (i % 3 = 1)
. . .
else // (i % 3 == 2)
. . .
在這個示例中,使用斷言會更好一些。
if (i % 3 == 0)
. . .
else if (i % 3 == 1)
. . .
else
{
assert i % 3 == 2;
. . .
}
當然,如果再仔細地考慮一下這個問題會發現一個更有意思的內容。i%3 會產生什么結果?如果 i 是正值,那余數肯定是 0、 1 或 2。如果 i 是負值,則余數則可以是 -1 和-2。然而,實際上都認為 i 是非負值, 因此, 最好在 if 語句之前使用下列斷言:assert i >= 0;
無論如何,這個示例說明了程序員如何使用斷言來進行自我檢查。前面已經知道,斷言是一種測試和調試階段所使用的戰術性工具; 而日志記錄是一種在程序的整個生命周期都可以使用的策略性工具。