一、簡介
在Java程序執行過程中, 總是會發生不被期望的事件, 阻止程序按照程序員預期正常運行, 這就是Java程序出現的異常。在企業級開發中, 容易導致各種各樣的小bug, 嚴重影響產品的運行和用戶體驗。所以大多數開發團隊都有自己的異常處理的規則和方法。如果你是一個團隊的新手,你可能會驚訝於這些方法與你之前使用過的那些方法有多么不同。
二、Java異常體系
Java所有異常的父類都是java.lang.Throwable、無論是內部的異常還是自定義異常。只有直接或者間接集成java.lang.Throwable類,JVM才會認為這是異常對象並且處理。
Java異常體系中Error為錯誤,較Exception嚴重。Error不是由我們程序自身導致的,是JVM運行錯誤導致的,所以暫時不是我們討論的范圍。Exception則是異常的基類,又可以分為"運行時異常"與"編譯時異常"(又稱為"非檢查異常和檢查異常")。
非檢查異常RuntimeException; 在編譯階段無法檢查,如ArithmeticException(除0引發)、InputMismatchException(輸入的數據不能被轉換為int類型引發)。引發非檢查異常大多數原因是編碼錯誤,應該檢查程序。
檢查異常(IOException),在編譯時可以檢查, 需要異常處理。處理方式有二種、(1)函數簽名中throws拋出異常 (2)tryCatch語句捕獲。
三 、異常現象
下面的代碼會演示2個異常類型:ArithmeticException 和 InputMismatchException。前者由於整數除0引發,后者是輸入的數據不能被轉換為int類型引發。
1 package it.check.exception; 2 import java.util.Scanner; 3 /* 4 * 初步測試Java的“非檢查異常”、如ArithmeticException(除0引發)、InputMismatchException(輸入的數據不能被轉換為int類型引發) 5 * “非檢查異常”在編譯時不會提示信息、在運行是則會拋出異常。通常不需要try{} catch(){} finally{}處理 6 * 7 * **/ 8 public class ArithmeticExceptionRun { 9 10 public static void main(String[] args) { 11 System . out. println( "----歡迎使用命令行除法計算器----" ) ; 12 CMDCalculate (); 13 14 } 15 public static void CMDCalculate () 16 { 17 Scanner scan = new Scanner ( System. in ); 18 int num1 = scan .nextInt () ; //阻塞、等待輸入 19 int num2 = scan .nextInt () ; 20 int result = devide (num1 , num2 ) ; 21 System . out. println( "result:" + result) ; 22 scan .close () ; 23 } 24 public static int devide (int num1, int num2 ){ 25 return num1 / num2 ; 26 } 27 28 }
/** * ----歡迎使用命令行除法計算器---- 2 0 Exception in thread "main" java.lang.ArithmeticException: / by zero at it.check.exception.ArithmeticExceptionRun.devide(ArithmeticExceptionRun.java:25) at it.check.exception.ArithmeticExceptionRun.CMDCalculate(ArithmeticExceptionRun.java:20) at it.check.exception.ArithmeticExceptionRun.main(ArithmeticExceptionRun.java:12) * * */ /** * ----歡迎使用命令行除法計算器---- a Exception in thread "main" java.util.InputMismatchException at java.util.Scanner.throwFor(Scanner.java:864) at java.util.Scanner.next(Scanner.java:1485) at java.util.Scanner.nextInt(Scanner.java:2117) at java.util.Scanner.nextInt(Scanner.java:2076) at it.check.exception.ArithmeticExceptionRun.CMDCalculate(ArithmeticExceptionRun.java:18) at it.check.exception.ArithmeticExceptionRun.main(ArithmeticExceptionRun.java:12) * */
異常追蹤棧; 異常在函數中產生,函數存在調用棧,main 調用CMDCalculate方法、CMDCalculate方法在調用device。當發生/0 異常時, 當這些被影響的函數以異常信息輸出時、形成異常追蹤棧、由device->CMDCalculate->main 棧頂向棧底回朔。
以上例子為非檢查異常、接着是檢查異常, 有二種處理方式。
方式一: 使用tryCatchFinally語句捕獲
public class BufferDemo { public static void main(String[] args) { bufferRead("E:"+File.separatorChar+"a.txt"); } /** 使用緩沖技術讀取數據 * * */ public static void bufferRead(String path) { BufferedInputStream bi=null; byte[] buf=new byte[1024]; int length=0; try { bi=new BufferedInputStream(new FileInputStream(path)); while((length=bi.read(buf))!=-1){ System.out.println(new String(buf,0,length)); } } catch (IOException e) { e.printStackTrace(); }finally { try { if(bi!=null){ bi.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
方式二:throws拋出,由調用者處理。函數使用throws拋出異常可能是(1)函數本身不知道怎么處理異常 (2)把異常交給調用者處理(捕獲等)更加合適。
public class BufferDemo { public static void main(String[] args) { try { bufferRead("E:"+File.separatorChar+"a.txt"); } catch (IOException e) { e.printStackTrace(); } } /** 使用緩沖技術讀取數據 * @throws IOException * * * */ public static void bufferRead(String path) throws IOException{ BufferedInputStream bi=null; byte[] buf=new byte[1024]; int length=0; bi=new BufferedInputStream(new FileInputStream(path)); while((length=bi.read(buf))!=-1){ System.out.println(new String(buf,0,length)); bi.close(); } } }
四、異常鏈化
在一些大型的,模塊化的軟件開發中,一旦一個地方發生異常,則如骨牌效應一樣,將導致一連串的異常。假設B模塊完成自己的邏輯需要調用A模塊的方法,如果A模塊發生異常,則B也將不能完成而發生異常,但是B在拋出異常時,會將A的異常信息掩蓋掉,這將使得異常的根源信息丟失。異常的鏈化可以將多個模塊的異常串聯起來,使得異常信息不會丟失。
異常鏈化:以一個異常對象為參數構造新的異常對象。新的異對象將包含先前異常的信息。這項技術主要是異常類的一個帶Throwable參數的函數來實現的。這個當做參數的異常,我們叫他根源異常(cause)。
查看Throwable類源碼,可以發現里面有一個Throwable字段cause,就是它保存了構造時傳遞的根源異常參數。這種設計和鏈表的結點類設計如出一轍,因此形成鏈也是自然的了。
public class Throwable implements Serializable { private Throwable cause = this; public Throwable(String message, Throwable cause) { fillInStackTrace(); detailMessage = message; this.cause = cause; } public Throwable(Throwable cause) { fillInStackTrace(); detailMessage = (cause==null ? null : cause.toString()); this.cause = cause; } //........ }
下面是一個例子,演示了異常的鏈化:從命令行輸入2個int,將他們相加,輸出。輸入的數不是int,則導致getInputNumbers異常,從而導致add函數異常,則可以在add函數中拋出一個鏈化的異常。給出鏈化例子。
1 public static void main(String[] args) 2 { 3 4 System.out.println("請輸入2個加數"); 5 int result; 6 try 7 { 8 result = add(); 9 System.out.println("結果:"+result); 10 } catch (Exception e){ 11 e.printStackTrace(); 12 } 13 } 14 //獲取輸入的2個整數返回 15 private static List<Integer> getInputNumbers() 16 { 17 List<Integer> nums = new ArrayList<>(); 18 Scanner scan = new Scanner(System.in); 19 try { 20 int num1 = scan.nextInt(); 21 int num2 = scan.nextInt(); 22 nums.add(new Integer(num1)); 23 nums.add(new Integer(num2)); 24 }catch(InputMismatchException immExp){ 25 throw immExp; 26 }finally { 27 scan.close(); 28 } 29 return nums; 30 } 31 32 //執行加法計算 33 private static int add() throws Exception 34 { 35 int result; 36 try { 37 List<Integer> nums =getInputNumbers(); 38 result = nums.get(0) + nums.get(1); 39 }catch(InputMismatchException immExp){ 40 throw new Exception("計算失敗",immExp); /////////////////////////////鏈化:以一個異常對象為參數構造新的異常對象。 41 } 42 return result; 43 } 44 45 /* 46 請輸入2個加數 47 r 1 48 java.lang.Exception: 計算失敗 49 at practise.ExceptionTest.add(ExceptionTest.java:53) 50 at practise.ExceptionTest.main(ExceptionTest.java:18) 51 Caused by: java.util.InputMismatchException 52 at java.util.Scanner.throwFor(Scanner.java:864) 53 at java.util.Scanner.next(Scanner.java:1485) 54 at java.util.Scanner.nextInt(Scanner.java:2117) 55 at java.util.Scanner.nextInt(Scanner.java:2076) 56 at practise.ExceptionTest.getInputNumbers(ExceptionTest.java:30) 57 at practise.ExceptionTest.add(ExceptionTest.java:48) 58 ... 1 more 59 60 */