Java 异常处理
Java的異常機制主要依賴于try、catch、finally、throw和throws五個關鍵字,
try關鍵字后緊跟一個花括號括起來的代碼塊(花括號不可省略),簡稱try塊,它里面放置可能引發異常的代碼
catch后對應異常類型和一個代碼塊,用于表明該catch塊用于處理這種類型的代碼塊
多個catch塊后還可以跟一個finally塊,finally塊用于回收在try塊里打開的物理資源,異常機制會保證finally塊總被執行
throws關鍵字主要在方法簽名中使用,拋出一個具體的異常對象
throw用于拋出一個實際的異常,throw可以單獨作為語句使用,拋出一個具體的異常對象
我們希望所有的錯誤都可以在編譯階段被發現,就是在試圖運行程序之前排除所有錯誤,但這是不現實的,余下的問題必須在運行期間得到解決。JAVA將異常分為兩種,Checked異常和Runtime異常,JAVA認為Checked異常都是可以在編譯階段被處理的異常,所以他強制程序處理所有多的Checked異常;而Runtime異常則無需處理
異常機制可以使程序中的異常處理代碼和正常業務代碼分離,保證程序代碼更加優雅,并可以提高程序的健壯性
Java異常機制
Java的異常處理機制可以讓程序具有極好的容錯性,讓程序更加健壯。當程序運行出現意外情形時,系統會自動生成一個Exception對象來通知程序,從而實現將“業務功能實現代碼”和“錯誤處理代碼”分離,提供更好的可讀性
使用try...catch捕獲異常
Java提出了一種假設:如果程序可以順利完成,那就“一切正常”,把系統的業務實現代碼放在try塊中定義,所有的異常處理邏輯放在catch塊中進行處理。下面是Java異常處理機制的語法結構
try {// 業務實現代碼... } catch (Exception) {alert 輸入不合法goto retry }如果執行try塊里業務邏輯代碼時出現異常,系統自動生成一個異常對象,該對象被提交給Java運行時環境,這個過程被稱為拋出(throw)異常。當Java運行時環境收到異常對象時,會尋找能處理該異常的catch塊,如果找到合適的catch塊,則把該異常對象交給catch塊處理,這個過程被稱為捕獲(catch)異常;如果Java運行時找不到捕獲異常的catch塊,則運行時環境終止,Java程序也將退出
String inputStr = null; // br.readLine():每當在鍵盤上輸入一行內容按回車, // 用戶剛剛輸入的內容將被br讀取到。 while ((inputStr = br.readLine()) != null) {try{// 將用戶輸入的字符串以逗號作為分隔符,分解成2個字符串String[] posStrArr = inputStr.split(",");// 將2個字符串轉換成用戶下棋的坐標int xPos = Integer.parseInt(posStrArr[0]);int yPos = Integer.parseInt(posStrArr[1]);// 把對應的數組元素賦為"●"。if (!gb.board[xPos - 1][yPos - 1].equals("╋")){System.out.println("您輸入的坐標點已有棋子了,"+ "請重新輸入");continue;}gb.board[xPos - 1][yPos - 1] = "●";}catch (Exception e){System.out.println("您輸入的坐標不合法,請重新輸入,"+ "下棋坐標應以x,y的格式");continue;}... }異常類的繼承體系
當Java運行時環境接收到異常對象時,catch關鍵字形式(Exception e)的每一個catch塊都會處理該異常類及其實例
當Java運行時環境接收到異常對象后,會依次判斷該異常對象是否是catch塊后異常類或其子類的實例,如果是,Java運行時環境將調用該catch塊來處理該異常;否則再次判斷該異常對象和下一個catch塊里的異常類進行比較
Java異常捕獲流程示意圖
當程序進入負責異常處理的catch塊時,系統生成的異常對象ex將會傳給catch塊后的異常形參,從而允許catch塊通過該對象來獲得異常的詳細信息
try塊后可以有多個catch塊,try塊后使用多個catch塊是為了針對不同異常類提供不同的異常處理方式。當系統發生不同的意外情況時,系統會生成不同的異常對象,Java運行時就會根據該異常對象所屬的異常類來決定使用哪個catch塊來處理該異常
通過在try塊后提供多個catch塊可以無須在異常處理塊中使用if、switch判斷異常類型,但依然可以針對不同異常類型提供相應的處理邏輯,從而提供更細致,更有調理的異常處理邏輯
從上圖可以看出,通常情況下,如果try塊被執行一次,則try塊后只有一個catch塊會被執行,絕不可能有多個catch塊被執行,除非在循環中使用了continue開始下一次循環,下一次循環又重新運行了try塊,這才可能導致多個catch塊被執行
try塊與if語句不一樣,try塊后的花括號({...})不可以省略,即使try塊里只有一行代碼,也不可以省略這個花括號。與之類似的,catch塊后的花括號({...})也不可以省略。還有一點需要指出:try塊里聲明的變量是代碼塊內局部變量,它只在try塊內有效,catch塊中不能訪問該變量。
Java常見的異常類之間的繼承關系圖
Java把所有非正常情況分成兩種:異常(Exception)和錯誤(Error),它們都是繼承Throwable父類
Error錯誤,一般是指與虛擬機(JVM)相關的問題,如系統崩潰、虛擬機出錯誤、動態鏈接失敗等,這種錯誤是java程序的根本運行環境出現了問題,這樣錯誤無法恢復或不可能捕獲,將導致應用程序中斷。通常應用程序無法處理這些錯誤,因此應用程序不應該試圖使用catch塊來捕獲Error對象。在定義該方法時,也無須在其throws子句聲明該方法能拋出Error及其任何子類
public class DivTest {public static void main(String[] args){try{int a = Integer.parseInt(args[0]);int b = Integer.parseInt(args[1]);int c = a / b;System.out.println("您輸入的兩個數相除的結果是:" + c );}catch (IndexOutOfBoundsException ie){System.out.println("數組越界:運行程序時輸入的參數個數不夠");}catch (NumberFormatException ne){System.out.println("數字格式異常:程序只能接受整數參數");}catch (ArithmeticException ae){System.out.println("算術異常");}catch (Exception e){System.out.println("未知異常");}} }如果運行該程序時輸入的參數不夠,將會發生數組越界異常,Java運行時將調用IndexOutOfBoundsException對應的catch塊處理該異常
如果運行該程序時輸入的參數不是數字,而是字母,將發生數字格式異常,Java運行時將調用NumberFormatException對應的catch塊處理該異常
如果運行該程序時輸入的第二個參數是0,將發生除0異常,Java運行時將調用ArithmeticException對應的catch塊處理該異常
如果運行該程序時出現其他異常,該異常對象總是Exception類或其子類的實例,Java運行時將調用Exception對應的catch塊處理該異常
如果運行該程序時試圖調用一個null對象的實例方法或實例變量時,Java運行時將調用NullPointerException對應的catch塊處理該異常
實際上,進行異常捕獲時不僅應該把Exception類對應的catch塊放在最后,而且所有父類異常的catch塊都應該排在子類異常catch塊的后面(先處理小異常,再處理大異常),否則將出現編譯錯誤
Java7提供的多異常捕獲
一個catch塊可以捕獲多鐘類型的異常,使用一個catch塊捕獲多鐘類型的異常時需要注意如下兩個地方
捕獲多鐘類型的異常時,多鐘異常之間用豎線(|)隔開
捕獲多鐘類型的異常時,異常變量有隱式的final修飾,因此程序不能對異常變量重新賦值
訪問異常信息
如果程序需要在catch塊中訪問異常對象的相關信息,則可以通過訪問catch塊的后異常形參來獲得。當Java運行時決定調用某個catch塊來處理該異常對象時,會將異常對象賦給catch塊后的異常參數,程序即可通過該參數來獲得異常相關信息
getMessage():返回該異常的詳細描述字符串
printStackTrace():將該異常的跟蹤棧信息輸出到標準錯誤輸出
printStackTrace(PrintStream s):將該異常的跟蹤棧信息輸出到標準錯誤輸出
getStackTrace():返回該異常的跟蹤棧信息
使用finally回收資源
有些時候,程序在try塊里打開了一些物理資源(例如數據庫連接、網絡連接和磁盤文件等),這些物理資源都必須顯式回收
Java垃圾回收機制不會回收任何物理資源,垃圾回收機制只能回收堆內存中對象所占用的內存
如果try塊的某條語句引起了異常,該語句后面的其他語句通常不會獲得執行的機會,這將導致位于該語句之后的資源回收語句得不到執行。如果在catch塊里進行資源回收,但catch塊完全有可能得不到執行,這將導致不能及時回收這些物理資源
為了保證一定能回收try塊中打開的物理資源,異常處理機制提供了finally塊。不管try塊中的代碼是否出現異常,也不管哪一個catch塊被執行,甚至在try塊或catch塊中執行了return語句,finally塊總會被執行
try {// 業務實現代碼... } catch (SubException1 e) {// 異常處理塊1... } catch (SubException2 e) {// 異常處理塊2... } ... finally {// 資源回收塊... }異常處理語法結構中,只有try塊是必須的,也就是說,如果沒有try塊,則不能有后面的catch塊和finally塊;catch塊和finally塊都是可選的,但catch塊和finally塊至少出現其中之一,也可以同時出現;可以有多個catch塊,捕獲父類異常的catch塊必須位于捕獲子類異常的后面;但不能只有try塊,既沒有catch塊,也沒有finally塊;多個catch塊必須位于try塊之后,finally塊必須位于所有的catch塊之后
public class FinallyTest {public static void main(String[] args){FileInputStream fis = null;try{fis = new FileInputStream("a.txt");}catch (IOException ioe){System.out.println(ioe.getMessage());// return語句強制方法返回return ; // ①// 使用exit來退出虛擬機// System.exit(1); // ②}finally{// 關閉磁盤文件,回收資源if (fis != null){try{fis.close();}catch (IOException ioe){ioe.printStackTrace();}}System.out.println("執行finally塊里的資源回收!");}} }除非在try塊、catch塊中調用了退出虛擬機的方法,否則不管在try塊、catch塊中執行怎樣的代碼,出現怎樣的情況,異常處理的finally塊總會被執行
在通常情況下,不要在finally塊中使用如return或throw等導致方法終止的語句,一旦在finally塊中使用了return或throw語句,將會導致try塊、catch塊中的return、throw語句失效
當Java程序執行try塊,catch塊時遇到了return或throw語句,這兩個語句都會導致該方法立即結束,但是系統執行這兩個語句并不會結束該方法,而是去尋找該異常處理流程中是否包含finally塊,如果沒有finally塊,程序立即執行return或throw語句,方法終止;如果有finally塊,系統立即開始執行finally塊——只有當finally塊執行完后,系統才會再跳回來執行try塊,catch塊里的return或throw語句。如果finally塊也使用了return或throw等導致方法終止的語句,finally塊已經終止了方法,系統將不會再跳回去執行try塊、catch塊里的任何代碼
盡量避免在finally里使用return或throw等導致方法終止的語句,否則可能出現一些很奇怪的情況
異常處理的嵌套
在try塊、catch塊或finally塊中包含完整的異常處理流程的情形被稱為異常處理的嵌套
異常處理流程代碼可以放在任何可執行性代碼的地方,因此完整的異常處理流程既可以放在try塊里,也可以放在catch塊里,還可以放在finally塊里
異常處理嵌套的深度沒有很明確的限制,但通常沒有必要使用超過兩層的嵌套異常處理,層次太深的嵌套異常處理沒有太大必要,而且導致程序可讀性降低
Java7的自動關閉資源的try語句
Java7增強了try語句的功能,它允許在try關鍵字后緊跟一對圓括號,圓括號可以聲明、初始化一個或多個資源,此處的資源指的是那些必須在程序結束時顯式關閉的資源(比如數據庫連接、網絡連接等),try語句在該語句結束時自動關閉這些資源。為了保證try語句可以正常關閉資源,這些資源實現類必須實現Closeable或AutoCloseable接口,實現這些類就必須實現close()方法
Closeable是AutoCloseable的子接口,可以被自動關閉的資源類要么實現AutoCloseable接口,要么實現Closeable接口。
Closeable接口里的close()方法聲明拋出了IOException,因此它的實現類在實現close()方法時只能聲明拋出IOException或其子類
AutoCloseable接口里的close()方法聲明拋出了Exception,因此它的實現類在實現close()方法時可以聲明拋出任何異常
下面程序示范如何使用自動關閉資源的try語句
public class AutoCloseTest {public static void main(String[] args)throws IOException{try (// 聲明、初始化兩個可關閉的資源// try語句會自動關閉這兩個資源。BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));PrintStream ps = new PrintStream(newFileOutputStream("a.txt"))){// 使用兩個資源System.out.println(br.readLine());ps.println("莊生曉夢迷蝴蝶");}} }上面程序圓括號里代碼分別聲明、初始化了兩個IO流,由于BufferedReader、PrintStream都實現了Closeable接口,而且它們放在try語句中聲明、初始化,所以try語句會自動關閉它們。因此程序是安全的。自動關閉資源的try語句相當于包含了隱式的finally塊(這個finally塊用于關閉資源),因此這個try語句可以既沒有catch塊,也沒有finally塊
自動關閉資源的try語句后也可以帶多個catch塊和一個finally塊
Checked異常和Runtime異常體系
Java的異常被分為兩大類:Checked異常和Runtime異常(運行時異常)。所有RuntimeException類及其子類實例被稱為Runtime異常;不是RuntimeException類及其子類的異常實例則稱為Checked異常
只有Java語言提供了Checked異常,其他語言都沒有提供Checked異常。Java認為Checked異常都是可以被處理(修復)的異常,所以Java程序必須顯式處理Checked異常
對于Checked異常的處理方式有兩種
當前方法明確知道如何處理該異常,程序應該使用try...catch塊來捕獲該異常,然后在對應的catch塊中修補該異常
當前方法不知道如何處理這種異常,應該在定義該方法時聲明拋出該異常
Runtime異常則更加靈活,Runtime異常無須顯式聲明拋出,如果程序需要捕捉Runtime異常,也可以使用try...catch塊來實現
使用throws聲明拋出異常
使用throws聲明拋出異常的思路是,當前方法不知道如何處理這種類型的異常,該異常應該由上一級調用者處理;如果main方法也不知道如何處理這種類型的異常,也可以使用throws聲明拋出異常,該異常交給JVM處理。JVM對異常處理的方法是打印異常的跟蹤棧信息,并中止程序運行,這就是前面程序在遇到異常后自動結束的原因
throws聲明拋出只能在方法簽名中進行使用,throws可以聲明拋出多個異常類,多個異常類之間以逗號隔開
throws Exception1, Exception2...下面使用了throws來聲明拋出IOException異常,一旦使用throws語句聲明拋出該異常,程序就無須使用try...catch塊來捕獲該異常了。程序聲明不處理IOException異常,將該異常交給JVM處理,所以程序一旦遇到該異常,JVM就會打印該異常的跟蹤棧信息,并結束程序
public class ThrowsTest {public static void main(String[] args)throws IOException{FileInputStream fis = new FileInputStream("a.txt");} }如果某段代碼中調用了一個帶throws聲明的方法,該方法聲明拋出了Checked異常,則表明該方法希望它的調用者來處理該異常。也就是說,調用該方法時要么放在try塊中顯式捕獲該異常,要么放在另一個帶throws聲明拋出的方法中
public class ThrowsTest2 {public static void main(String[] args)throws Exception{// 因為test()方法聲明拋出IOException異常,// 所以調用該方法的代碼要么處于try...catch塊中,// 要么處于另一個帶throws聲明拋出的方法中。test();}public static void test()throws IOException{// 因為FileInputStream的構造器聲明拋出IOException異常,// 所以調用FileInputStream的代碼要么處于try...catch塊中,// 要么處于另一個帶throws聲明拋出的方法中。FileInputStream fis = new FileInputStream("a.txt");} }子類方法聲明拋出的異常類型應該是父類方法聲明拋出的異常類型的子類或相同,子類方法聲明拋出的異常不允許比父類方法聲明拋出的異常多
public class OverrideThrows {public void test()throws IOException{FileInputStream fis = new FileInputStream("a.txt");} } class Sub extends OverrideThrows {// 子類方法聲明拋出了比父類方法更大的異常// 所以下面方法出錯public void test()throws Exception{} }使用Checked異常至少存在如下兩大不便之處
對于程序中的Checked異常,java要求必須顯式捕獲并處理該異常,或者顯式聲明拋出該異常。這樣就增加了編程復雜度
如果在方法中顯式聲明拋出Checked異常,將會導致方法簽名與異常耦合,如果該方法是重寫父類的方法,則該方法拋出的異常還會受到被重寫方法所拋出異常的限制
在大部分時候推薦使用Runtime異常,而不使用Checked異常。尤其當程序需要自行拋出異常時,使用Runtime異常更加簡潔。當使用Runtime異常時,程序無須在方法中聲明拋出Checked異常,一旦發生了自定義錯誤,程序只管拋出Runtime異常即可
使用throw拋出異常
當程序出現錯誤時,系統會自動拋出異常;Java允許程序自行拋出異常,自行拋出異常使用throw語句來完成。
拋出異常
throw語句可以單獨使用,throw語句拋出的不是異常類,而是一個異常實例,而且每次只能拋出一個異常實例。throw語句的語法格式如下:
throw ExceptionInsantance;當Java運行時接收到開發者自行拋出的異常時,同樣會中止當前的執行流,跳到該異常對應的catch塊,由該catch塊來處理該異常。也就是說,不管是系統自動拋出的異常,還是程序員手動拋出的異常,Java運行時環境對異常的處理沒有任何差別
如果throw語句拋出的異常是Checked異常,則該throw語句要么處于try塊里,顯式捕獲該異常,要么放在一個帶throws聲明拋出的方法中,即把該異常交給該方法的調用者處理;如果throw語句拋出的異常是Runtime異常,則該語句無須放在try塊里,也無須放在帶throws聲明拋出的方法中;程序既可以顯式使用try…catch來捕獲并處理該異常,也可以完全不理會該異常,把該異常交給方法調用者處理
public class ThrowTest {public static void main(String[] args){try{// 調用聲明拋出Checked異常的方法,要么顯式捕獲該異常// 要么在main方法中再次聲明拋出throwChecked(-3);}catch (Exception e){System.out.println(e.getMessage());}// 調用聲明拋出Runtime異常的方法既可以顯式捕獲該異常,// 也可不理會該異常throwRuntime(3);}public static void throwChecked(int a)throws Exception{if (a > 0){// 自行拋出Exception異常// 該代碼必須處于try塊里,或處于帶throws聲明的方法中throw new Exception("a的值大于0,不符合要求");}}public static void throwRuntime(int a){if (a > 0){// 自行拋出RuntimeException異常,既可以顯式捕獲該異常// 也可完全不理會該異常,把該異常交給該方法調用者處理throw new RuntimeException("a的值大于0,不符合要求");}} }自行拋出Runtime異常比自行拋出Checked異常的靈活性更好。拋出Checked異常則可以讓編譯器提醒程序員必須處理該異常
自定義異常類
在通常情況下,程序很少會自行拋出系統異常,因為異常的類名通常也包含了該異常的有用信息。所以在選擇拋出異常時,應該選擇合適的異常類,從而可以明確地描述該異常情況。在這種情形下,應用程序常常需要拋出自定義異常
自定義異常都應該繼承Exception基類,如果希望自定義Runtime異常,則應該繼承RuntimeException基類。定義異常類時通常需要提供兩個構造器:一個是無參數的構造器;另一個是帶一個字符串參數的構造器,這個字符串將作為該異常對象的描述信息(也就是異常對象的getMessage()方法的返回值)
public class AuctionException extends Exception {// 無參構造器public AuctionException(){}// 帶一個字符串參數的構造器public AuctionException(String msg){super(msg);} }在大部分情況下,創建自定義異常都可采用與AuctionException.java相似的代碼完成,只需改變AuctionException異常的類名即可,讓該異常類的類名可以準確描述該異常
catch和throw同時使用
兩種異常處理方式
在出現異常的方法內捕獲并處理異常,該方法的調用者將不能再次捕獲該異常
該方法簽名中聲明拋出該異常,將該異常完全交給方法調用者處理
當一個異常出現時,單靠某個方法無法完全處理該異常,必須由幾個方法協作才可以完全處理該異常。也就是說,在異常出現的當前方法中,程序只對異常進行部分處理,還有些處理需要在該方法的調用者中才能完成,所以應該再次拋出異常,讓該方法的調用者也能捕獲到異常
為了實現這種通過多個方法協作處理同一異常的情形,可以在catch塊中結合throw語句來完成
public class AuctionTest {private double initPrice = 30.0;// 因為該方法中顯式拋出了AuctionException異常,// 所以此處需要聲明拋出AuctionException異常public void bid(String bidPrice)throws AuctionException{double d = 0.0;try{d = Double.parseDouble(bidPrice);}catch (Exception e){// 此處完成本方法中可以對異常執行的修復處理,// 此處僅僅是在控制臺打印異常跟蹤棧信息。e.printStackTrace();// 再次拋出自定義異常throw new AuctionException("競拍價必須是數值,"+ "不能包含其他字符!");}if (initPrice > d){throw new AuctionException("競拍價比起拍價低,"+ "不允許競拍!");}initPrice = d;}public static void main(String[] args){AuctionTest at = new AuctionTest();try{at.bid("df");}catch (AuctionException ae){// 再次捕捉到bid方法中的異常。并對該異常進行處理System.err.println(ae.getMessage());}} }這種catch和throw結合使用的情況在大型企業級應用中非常常用。企業級應用對異常的處理通常分成兩個部
應用后臺需要通過日志來記錄異常發生的詳細情況
應用還需要根據異常向應用傳達某種提示
在這種情形下,所有異常都需要兩個方法共同完成,也就必須將catch和throw結合使用
異常鏈
對于真實的企業級應用而言,常常有嚴格的分層關系,層與層之間有非常清晰的劃分,上層功能的實現嚴格依賴于下層的API,也不會跨層訪問。下圖顯式了這種具有分層結構應用的大致示意圖
程序先捕獲原始的異常,然后拋出一個新的業務異常,新的業務異常中包含了對用戶的提示信息,這種處理方式被稱為異常轉譯
public calSal throws SalException {try{//實現結算工資的業務邏輯....}catch(SQLException sqle){//把原始異常記錄下來,留個管理員...//下面異常中的message就是向用戶的提示throw new SalException("訪問底層數據庫出現異常");}catch(Exception e){//把原始異常記錄下來,留個管理員....//下面異常中的message就是向用戶的提示throw new SalException("系統出現未知異常");} }這種把捕獲一個異常然后接著拋出另一個異常,并把原始異常信息保存下來是一種典型的鏈式處理(職責鏈模式),也稱為“異常鏈”
所有的Throwable子類在構造器中都可以接受一個cause對象作為參數。這個cause就用來表示原始異常,這樣可以把原始異常傳遞給新的異常,使得即使在當前位置創建并拋出了新的異常,你也能通過這個異常鏈追蹤到異常最初發生的位置。如果我們希望上面SalException可以追蹤到最原始的異常信息。則可以將該方法改寫如下
public calSal throws SalException {try{//實現結算工資的業務邏輯....}catch(SQLException sqle){//把原始異常記錄下來,留個管理員...//下面異常中的message就是向用戶的提示throw new SalException(sqle);}catch(Exception e){//把原始異常記錄下來,留個管理員....//下面異常中的message就是向用戶的提示throw new SalException(e);} }上面程序中創建SalException對象時,傳入了一個Exception對象,而不是傳入了一個String對象,這就需要SalException類有相應的構造器。從JDK1.4以后,Throwable基類有一個可以接收Exception參數的方法,所以可以采用如下代碼來定義SalException類
public class SalException extends Exception {public SalException(){}public SalException(String msg){super(msg);}public SalException(Throwable t){super(t);} }Java的異常跟蹤棧
異常對象的printStackTrace()方法用于打印異常的跟蹤棧信息,根據printStackTrace()方法的輸出結果,開發者可以找到異常的源頭,并跟蹤到異常一路觸發的過程
class SelfException extends RuntimeException {SelfException(){}SelfException(String msg){super(msg);} } public class PrintStackTraceTest {public static void main(String[] args){firstMethod();}public static void firstMethod(){secondMethod();}public static void secondMethod(){thirdMethod();}public static void thirdMethod(){throw new SelfException("自定義異常信息");} }從結果可知,異常從thirdMethod方法開始觸發,傳到secondMethod方法,再傳到firstMethod方法,最后傳到main方法,在main方法終止,這個過程就是Java的異常跟蹤棧
Exception in thread "main" SelfException: 自定義異常信息 at PrintStackTraceTest.thirdMethod(PrintStackTraceTest.java:25) at PrintStackTraceTest.secondMethod(PrintStackTraceTest.java:21) at PrintStackTraceTest.firstMethod(PrintStackTraceTest.java:17) at PrintStackTraceTest.main(PrintStackTraceTest.java:13)只要異常沒有被完全捕獲(包括異常沒有捕獲,或異常被處理后重新拋出了新異常),異常從發生異常的方法逐漸向外傳播,首先傳給該方法的調用者,該方法調用者再次傳給其調用者...直至最后傳到main方法,如果main方法依然沒有處理該異常,JVM會中止該程序,并打印異常的跟蹤棧信息
跟蹤棧記錄程序中所有的異常發生點,各行顯式被調用方法中執行的停止位置,并標明類、類中的方法名、與故障點對應的文件的行。一行行地往下看,跟蹤棧總是最內部的被調用方法逐漸上傳,直到最外部業務操作的起點,通常就是程序的入口main方法或Thread類的run方法(多線程)
public class ThreadExceptionTest implements Runnable {public void run(){firstMethod();}public void firstMethod(){secondMethod();}public void secondMethod(){int a = 5;int b = 0;int c = a / b;}public static void main(String[] args){new Thread(new ThreadExceptionTest()).start();} }程序在Thread的run方法中出現了ArithmeticException異常,這個異常的源頭是ThreadExceptionTest的secondMethod方法。這個異常傳播到Thread類的run方法就會結束(如果該異常沒有得到處理,將會導致該線程中止運行)
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero at ThreadExceptionTest.secondMethod(ThreadExceptionTest.java:15) at ThreadExceptionTest.firstMethod(ThreadExceptionTest.java:9) at ThreadExceptionTest.run(ThreadExceptionTest.java:5) at java.lang.Thread.run(Unknown Source)異常處理規則
使程序代碼混亂最小化
捕獲并保留診斷信息
通知合適的人員
采用合適的方式結束異常活動
不要過度使用異常
過度使用異常主要有兩個方面:
把異常和普通錯誤混淆在一起,不再編寫任何錯誤處理代碼,而是以簡單地拋出異常來代替所有的錯誤處理
使用異常處理來代替流程控制
異常處理機制的初衷是將不可預期的處理代碼和正常的業務邏輯處理代碼分離,因此絕不要使用異常處理來代替正常的業務邏輯判斷。另外,異常機制的效率比正常的流程控制效率差,所以不要使用異常處理來代替正常的程序流程控制
異常只應該用于處理非正常的情況,不要使用異常處理來代替正常的流程控制。對于一些完全可預知,而且處理方式清楚的錯誤,程序應該提供相應的錯誤處理代碼,而不是將其籠統地稱為異常
不要使用過于龐大的try塊
正確的做法是,把大塊的try塊分割成多個可能出現異常的程序段落,并把它們放在單獨的try塊中,從而分別捕獲并處理異常
避免使用Catch All語句
所謂Catch All語句指的是一種異常捕獲模塊,它可以處理程序發生的所有可能異常
try {// code here with checked exceptions } catch (Throwable t) {//exception handlert.printStackTrace(); }這種處理方式有如下兩點不足之處:
所有異常都采用相同的處理方式,這將導致無法對不同異常分情況處理,如果要分情況處理,則需要在catch塊中使用分支語句進行控制,這是得不償失的做法。
這種捕獲方式可能將程序中的錯誤、Runtime異常等可能導致程序終止的情況全部捕獲到,從而 “壓制”了異常。如果出現了一些“關鍵”異常,那個異常也會被“靜悄悄”地忽略
不要忽略捕獲到的異常
通常建議對異常進行適當措施:
處理異常。對異常采用合適的修補,然后繞過異常發生的地方繼續執行;或者用別的數據進行計算,以代替期望的方法返回值;或者提示用戶重新操作......總之,對于Checked異常,程序應該盡量采用修復
重新拋出新異常。把當前運行環境下能做的事情盡量作完,然后進行異常轉譯,把異常包裝成當前層的異常,重新拋出給上層調用者
在合適的層處理異常。如果當前層不清楚如何處理異常,就不要在當前層使用catch語句來捕獲該異常,直接使用throws聲明拋出該異常,讓上層調用者來負責處理該異常
總結
- 上一篇: 一个复杂系统的拆分改造实践
- 下一篇: 第三方提权之serv-u提权