3、假設程序員考慮到了寫異常處理代碼,異常處理代碼可能會使用if-elses語句跟主邏輯代碼混合在一塊,這就使得主邏輯比較混亂,程序閱讀性差。例如:
if (file exists) { open file; while (there is more records to be processed) { if (no IO errors) { process the file record } else { handle the errors } } if (file is opened) close the file; } else { report the file does not exist; }
下面我們來詳細說說這三個點:
1、異常必須被聲明
例如,假設你想使用java.util.Scanner得到硬盤文件格式化的輸入,Scanner構造函數如下:
public Scanner(File source) throws FileNotFoundException;它聲明了該方法可能會產生文件不存在的異常,通過在方法上聲明該異常,這樣在使用這個方法的時候,程序員就需要處理這個異常。
import java.util.Scanner; import java.io.File; public class ScannerFromFile { public static void main(String[] args) { Scanner in = new Scanner(new File("test.in")); // do something ... } }上面代碼沒有處理異常,編譯器會提示如下錯誤:
ScannerFromFile.java:5: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown Scanner in = new Scanner(new File("test.in")); ^
import java.util.Scanner; import java.io.File; import java.io.FileNotFoundException; public class ScannerFromFileWithCatch { public static void main(String[] args) { try { Scanner in = new Scanner(new File("test.in")); // do something if no exception ... // you main logic here in the try-block } catch (FileNotFoundException ex) { // error handling separated from the main logic ex.printStackTrace(); // print the stack trace } } }如果沒有找到文件,異常就會在catch塊中產生,在上面例子中它只是打印出了棧信息,它提供了有用的調試信息,在有些情況下,你需要做一些清除操作,或者打開另一個文件,可以看到上面的處理邏輯與主邏輯是分開的。
import java.util.Scanner; import java.io.File; import java.io.FileNotFoundException; public class ScannerFromFileWithThrow { public static void main(String[] args) throws FileNotFoundException { // to be handled by next higher-level method Scanner in = new Scanner(new File("test.in")); // this method may throw FileNotFoundException // main logic here ... } }上面這個例子中,並沒有使用try-catch塊去處理FileNotFoundException異常,取而代之的是在它的調用方法main上面去對這個異常進行了聲明,這就意味著這個異常會拋到調用棧中下一個更高級別的方法中去,在這個例子中,main()的下一個更高級別的方法是JVM,它會簡單的終止程序並且打印棧信息。
try { // Main logic here open file; process file; ...... } catch (FileNotFoundException ex) { // Exception handlers below // Exception handler for "file not found" } catch (IOException ex) { // Exception handler for "IO errors" } finally { close file; // always try to close the file }
public class MethodCallStackDemo { public static void main(String[] args) { System.out.println("Enter main()"); methodA(); System.out.println("Exit main()"); } public static void methodA() { System.out.println("Enter methodA()"); methodB(); System.out.println("Exit methodA()"); } public static void methodB() { System.out.println("Enter methodB()"); methodC(); System.out.println("Exit methodB()"); } public static void methodC() { System.out.println("Enter methodC()"); System.out.println("Exit methodC()"); } }
Enter main() Enter methodA() Enter methodB() Enter methodC() Exit methodC() Exit methodB() Exit methodA() Exit main()
假設我們在methodC()中執行除以0的操作,它會引發一個ArithmeticException異常。
public static void methodC() { System.out.println("Enter methodC()"); System.out.println(1 / 0); // divide-by-0 triggers an ArithmeticException System.out.println("Exit methodC()"); }異常消息很清楚的顯示了方法調用棧信息並且還是方法的行數。
Enter main() Enter methodA() Enter methodB() Enter methodC() Exception in thread "main" java.lang.ArithmeticException: / by zero at MethodCallStackDemo.methodC(MethodCallStackDemo.java:22) at MethodCallStackDemo.methodB(MethodCallStackDemo.java:16) at MethodCallStackDemo.methodA(MethodCallStackDemo.java:10) at MethodCallStackDemo.main(MethodCallStackDemo.java:4) MethodC()觸發了一個ArithmeticException異常,因為它沒有處理這個異常,因此它就會立即出棧,MethodB()也不能處理這個異常並且它也會出棧, methodA() 和 main()也相同,main()會返回到JVM,它會終止程序並且向上面一樣打印棧信息。
1.3 異常與調用棧
當java方法中出現一個異常,這個方法會創建一個Exception對象,並且傳遞這個Exception對象到JVM,Exception對象包含了異常的類型以及異常發生時程序的狀態,JVM的責任就是尋找處理這個Exception對象的異常處理者,它在調用棧中不斷的向後搜索直到找到一個匹配的異常處理者來處理這個Exception對象,如果JVM在方法棧的所有方法中沒有找到一個匹配的異常處理者,它就會終止程序。
整個過程可以想象如下:假設methodD()發生了一個異常事件並且向JVM拋出了一個XxxException,JVM會在調用棧中向後搜索匹配的異常處理者,它一步一步沿著調用棧向後尋找,發現methodA()有一個XxxException處理者,並且會將這個異常對象傳遞給這個異常處理者者。這裡需要注意的是,methodC() 和 methodB()被遍歷到了,只是它們沒有對異常處理,所以它們的方法會聲明一個throws XxxException。
1.4 異常類 - Throwable, Error, Exception & RuntimeException
下圖顯示了Exception類的繼承圖,所有異常對象的基類是java.lang.Throwable,它包含有兩個子類,java.lang.Exception 和 java.lang.Error。
Error表示系統內部異常(例如:VirtualMachineError, LinkageError),這種異常一般很少發生,如果這種異常發生,你基本上無法處理,程序會在終止。
Exception表示程序異常(例如:FileNotFoundException, IOException),這種異常可以被捕獲並且處理。
1.5 可檢查與不可檢查異常
我們都知道Error的子類以及RuntimeException是不可檢查異常,這些異常是編譯器無法檢查的,因此它們不需要被捕捉或者在方法中聲明,因為這些異常例如除以0會導致一個ArithmeticException,索引越界會導致ArrayIndexOutOfBoundException都是程序的邏輯錯誤,都是可以避免的,應該在程序中進行修正而不是進行運行時異常處理。
其他的異常都是可檢查的異常,它們可以被編譯器檢查到,並且進行捕獲或者在方法中聲明拋出。
1.6 異常處理操作
在異常處理中有5個關鍵字:try, catch, finally, throws 和 throw,需要注意throws 和 throw的不同。
Java的異常處理包含以下三種操作:
(1)聲明異常
(2)拋出異常
(3)捕獲異常
聲明異常
Java方法必須對可檢查異常進行聲明,使用關鍵字throws。
例如,假設methodD()定義如下:
public void methodD() throws XxxException, YyyException { // method body throw XxxException and YyyException }上面方法表示在調用methodD()可能會遇到兩種可檢查異常:XxxException 和 YyyException,換句話說,methodD()方法的內部可能會觸發XxxException 或者 YyyException。
拋出異常
當一個Java操作遇到一個異常,出現錯誤的語句可以創建一個指定的Exception對象並且通過throw XxxException語句將它拋給Java運行時。
例如:
public void methodD() throws XxxException, YyyException { // method's signature // method's body ... ... // XxxException occurs if ( ... ) throw new XxxException(...); // construct an XxxException object and throw to JVM ... // YyyException occurs if ( ... ) throw new YyyException(...); // construct an YyyException object and throw to JVM ... }
public void methodD() throws XxxException, YyyException { ...... }
public void methodC() { // no exception declared ...... try { ...... // uses methodD() which declares XxxException & YyyException methodD(); ...... } catch (XxxException ex) { // Exception handler for XxxException ...... } catch (YyyException ex} { // Exception handler for YyyException ...... } finally { // optional // These codes always run, used for cleaning up ...... } ...... }
public void methodC() throws XxxException, YyyException { // for next higher-level method to handle ... // uses methodD() which declares "throws XxxException, YyyException" methodD(); // no need for try-catch ... }在上面這個例子中,如果XxxException 或者 YyyException被methodD()拋出,JVM就會終止methodD()並且methodC()會將異常傳遞給調用棧中methodC()的調用者。
1.7 try-catch-finally
try-catch-finally的語法如下:
try { // main logic, uses methods that may throw Exceptions ...... } catch (Exception1 ex) { // error handler for Exception1 ...... } catch (Exception2 ex) { // error handler for Exception1 ...... } finally { // finally is optional // clean up codes, always executed regardless of exceptions ...... }(1)如果在執行try塊中的內容的時候沒有異常發生,所有catch塊中的邏輯將會跳過,在執行try塊邏輯之後finally塊將會執行。如果try塊中拋出一個異常,Java運行時就會不顧try塊後面的內容直接跳到catch塊中去尋找對應的異常處理者,它會匹配每個catch塊中的異常類型,如果某個catch塊的異常類型跟拋出異常的類型相同或者是拋出異常類型的父類,那麼匹配成功,這個catch就會執行,在執行完catch塊內容執行接著就會執行finally塊裡面的邏輯,執行完了finally塊裡面的邏輯之後就會接著執行後面的邏輯了。
import java.util.Scanner; import java.io.File; import java.io.FileNotFoundException; public class TryCatchFinally { public static void main(String[] args) { try { // main logic System.out.println("Start of the main logic"); System.out.println("Try opening a file ..."); Scanner in = new Scanner(new File("test.in")); System.out.println("File Found, processing the file ..."); System.out.println("End of the main logic"); } catch (FileNotFoundException ex) { // error handling separated from the main logic System.out.println("File Not Found caught ..."); } finally { // always run regardless of exception status System.out.println("finally-block runs regardless of the state of exception"); } // after the try-catch-finally System.out.println("After try-catch-finally, life goes on..."); } }當FileNotFoundException被觸發的時候輸出如下:
Start of the main logic Try opening a file ... File Not Found caught ... finally-block runs regardless of the state of exception After try-catch-finally, life goes on...
Start of the main logic Try opening a file ... File Found, processing the file ... End of the main logic finally-block runs regardless of the state of exception After try-catch-finally, life goes on...
例子2
public class MethodCallStackDemo { public static void main(String[] args) { System.out.println("Enter main()"); methodA(); System.out.println("Exit main()"); } public static void methodA() { System.out.println("Enter methodA()"); try { System.out.println(1 / 0); // A divide-by-0 triggers an ArithmeticException - an unchecked exception // This method does not catch ArithmeticException // It runs the "finally" and popped off the call stack } finally { System.out.println("finally in methodA()"); } System.out.println("Exit methodA()"); } }
Enter main() Enter methodA() finally in methodA() Exception in thread "main" java.lang.ArithmeticException: / by zero at MethodCallStackDemo.methodA(MethodCallStackDemo.java:11) at MethodCallStackDemo.main(MethodCallStackDemo.java:4)
catch (AThrowableSubClass aThrowableObject) { // exception handling codes }(4)對於catch塊中的參數throwable對象,可以使用下面方法來得到異常的類型和程序的調用狀態:
try { Scanner in = new Scanner(new File("test.in")); // process the file here ...... } catch (FileNotFoundException ex) { ex.printStackTrace(); }也可以使用printStackTrace(PrintStream s) 或者 printStackTrace(PrintWriter s).
public static void main(String[] args) throws Exception { // throws all subclass of Exception to JRE Scanner in = new Scanner(new File("test.in")); // declares "throws FileNotFoundException" ...... // other exceptions }方法的重新和重載
1.8 常用異常類
ArrayIndexOutOfBoundsException:如果訪問的數組索引超出了數組的限制,就會被JVM拋出。int[] anArray = new int[3]; System.out.println(anArray[3]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3NullPointerException:當代碼嘗試使用一個對象的空引用的時候,就會被JVM拋出。
String[] strs = new String[3]; System.out.println(strs[0].length());
Exception in thread "main" java.lang.NullPointerExceptionNumberFormatException: 當嘗試將一個字符串轉換為一個數字類型,但是字符串沒有合適的轉換方法。
Integer.parseInt("abc");
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"ClassCastException: 當對象類型轉換失敗的時候,就會被JVM拋出。
Object o = new Object(); Integer i = (Integer)o;
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.IntegerIllegalArgumentException:當一個方法接受到一個非法或者不合適的參數的時候就會拋出,我們可以在自己的代碼中重用這個異常。
// Create our own exception class by subclassing Exception. This is a checked exception public class MyMagicException extends Exception { public MyMagicException(String message) { //constructor super(message); } } public class MyMagicExceptionTest { // This method "throw MyMagicException" in its body. // MyMagicException is checked and need to be declared in the method's signature public static void magic(int number) throws MyMagicException { if (number == 8) { throw (new MyMagicException("you hit the magic number")); } System.out.println("hello"); // skip if exception triggered } public static void main(String[] args) { try { magic(9); // does not trigger exception magic(8); // trigger exception } catch (MyMagicException ex) { // exception handler ex.printStackTrace(); } } }輸出結果:
hello MyMagicException: you hit the magic number at MyMagicExceptionTest.magic(MyMagicExceptionTest.java:6) at MyMagicExceptionTest.main(MyMagicExceptionTest.java:14)
2. 斷言 (JDK 1.4)
JDK 1.4引入了一個新的關鍵字叫做"assert",它支持的就是斷言特性,Assertion就是用來檢測你關於程序邏輯的假設(例如:前提條件,後置條件和不變關系)。每個斷言包含一個boolean表達式,如果為true表示假設與執行結果相同,否則JVM就會拋出一個AssertionError。它表示你有一個錯誤的假設,它需要被修正,使用斷言比使用if-else表達式更好,它是對你假設的有效說明,並且它不影響程序性能。
assert聲明有兩種形式:
assert booleanExpr; assert booleanExpr : errorMessageExpr;
public class AssertionSwitchTest { public static void main(String[] args) { char operator = '%'; // assumed either '+', '-', '*', '/' only int operand1 = 5, operand2 = 6, result = 0; switch (operator) { case '+': result = operand1 + operand2; break; case '-': result = operand1 - operand2; break; case '*': result = operand1 * operand2; break; case '/': result = operand1 / operand2; break; default: assert false : "Unknown operator: " + operator; // not plausible here } System.out.println(operand1 + " " + operator + " " + operand2 + " = " + result); } }
> javac AssertionSwitchTest.java // no option needed to compile > java -ea AssertionSwitchTest // enable assertion
Exception in thread "main" java.lang.AssertionError: % at AssertionSwitchTest.main(AssertionSwitchTest.java:11)
> java AssertionSwitchTest // assertion disable by default
5 % 6 = 0
default: throw new AssertionError("Unknown operator: " + operator);
public class AssertionTest { public static void main(String[] args) { int number = -5; // assumed number is not negative // This assert also serve as documentation assert (number >= 0) : "number is negative: " + number; // do something System.out.println("The number is " + number); } }
> java -ea AssertionSwitchTest // enable assertion
Exception in thread "main" java.lang.AssertionError: -5 at AssertionTest.main(AssertionTest.java:5)斷言可以用來對程序進行驗證:
// Constructor of Time class public Time(int hour, int minute, int second) { if(hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) { throw new IllegalArgumentException(); } this.hour = hour; this.minute = minute; this.second = second; }