文章目錄
- 異常基礎知識
- 性能問題
- 異常聲明
- 異常問題
- 異常的缺陷
- JDK7異常新特性
- 抛出多個異常
- 無法捕獲的異常
異常基礎知識
- 異常機制的五個關鍵字
,try
,catch
,finally
,throw
throws
- java中異常的思想:用類的形式對不正常的情況進行了描述和封裝對象,這種類稱為異常類。不同的問題使用不同的類進行描述,實作流程代碼與問題處理代碼分離。
-
:不管程式代碼塊中是否處于抛出異常
中,甚至包括try
塊中的代碼,隻要執行該代碼塊時出現了異常,系統總會自動生成一個異常對象,該異常對象被送出給java運作時環境,這個過程被稱為catch
。當異常被抛出後,JVM會在抛出異常的方法中尋找最近的比對的catch 語句,如果沒有則在調用方法中找,直至周遊調用棧中所有方法位置。如果沒有找到任何比對的抛出異常(throw Exception)
語句,則會調用catch
方法ThreadGroup.uncaughtException()
- throw自行抛出異常,可單獨使用,throw語句抛出的不是異常類,而是一個異常類型,而且每次隻能抛出一個異常執行個體。
-
主要在方法簽名中使用,用于聲明該方法可能抛出的異常(ps:加s表示抛一堆),throws
用于抛出一個實際的異常,可單獨作為語句使用,抛出一個具體的異常對象。子類方法聲明抛出的異常類型應該是父類方法聲明抛出的異常類型的子類或相同,子類方法聲明抛出的異常不允許比父類方法聲明抛出的異常多。throw
-
塊中聲明的變量是代碼塊内局部變量,外界不可通路。通常情況下try
塊被執行一次,則try
塊後隻有一個try
塊會被執行,絕不可能有多個catch
塊執行。除非在循環中使用continue開始下一次循環,下一次循環又重新運作catch
塊,這才可能導緻多個try
塊被執行。父類異常的catch都應該在子類異常catch塊的後面。catch
-
:一般指與虛拟機相關的問題,如系統崩潰、虛拟機錯誤、動态連結庫失敗等,這種錯誤無法恢複或不可能捕獲,将導緻應用程式中斷Error
-
:不管try中的代碼是否出現異常,也不管哪一個catch塊被執行,甚至try或catch中執行了return語句,finally塊總會被執行(一些特殊情況除外)。異常處理結果中try是必須的,但catch和finally塊至少出現其中之一。當要把記憶體之外的資源恢複到他們初始狀态時,就要用到finally子句。包括已經打開的檔案或網絡連結,在螢幕上畫出的圖形。finally
public class FinallyTest { static int count = 0; /** * 發生了異常 * 在finally中發生 * 沒有異常 * 在finally中發生 */ public static void main(String[] args) { while (true) { try { if (count++ == 0) { throw new Exception("發生了異常"); } System.out.println("沒有異常"); } catch (Exception e) { System.out.println(e.getMessage()); } finally { System.out.println("在finally中發生"); if (count == 2) { break; } } } } }
- 除非在
或者try
塊中調用了退出虛拟機的方法(catch
),否則不管System.exit(0)
中執行怎樣的代碼,出現怎樣的情況,異常處理的try-catch
塊總會被執行。通常情況下不要在finally
塊中使用如finally
等導緻方法終止的語句,一旦return或throw
塊中使用可finally
語句,将導緻try塊catch塊中的return、throw語句失效。另一方面,如果在 try 塊執行期間撥掉電源,finally 也不會執行。return或throw
-
不會執行的幾種情況finally
- JVM過早中止(調用System.exit(0));
- 在finally塊中抛出一個未處理的異常
- 計算機斷電失火或遭遇病毒攻擊
public class Finally2Test { //什麼也不會輸出 public static void testFinally2() { try { System.exit(0); } finally { System.out.println("執行finally"); } } public static void main(String[] args) { testFinally2(); } } public class Finally3Test { /** * 輸出結果:執行finally */ public static void testFinally() { //ThreadDeath 是一個Error,以上代碼表示當JVM退出的時候抛出一個Error。是以finally執行了 System.setSecurityManager(new SecurityManager() { @Override public void checkExit(int status) { throw new ThreadDeath(); } }); try { System.exit(0); } finally { System.out.println("執行finally"); } } public static void main(String[] args) { testFinally(); } }
-
:沒有完善錯誤處理的代碼根本就不會被執行!checked降低了程式開發的生産效率和代碼的執行效率。在java語言規範中,将任何Error的子類以及RuntimeException的子類都稱為未檢查異常,而其他的異常都被稱為檢查異常Checked Exception(未檢查異常)
- 自定義異常
- 提供一個無參數的構造器
- 提供一個帶字元串參數的構造器,這個字元串将作為該異常對象的描述資訊
1. Exception沒有定義任何方法,但它繼承了Throwable提供的方法。 public class MyException extends Exception{ public MyException(){} public MyException(String name){ //調用父類構造器 super(msg); } }
- 重抛異常
- 重新抛異常會把異常抛給上一級環境中的異常處理程式,同一個try塊的後續catch子句将被忽略。此外異常對象的所有資訊都得以保持,是以高一級環境中捕獲此異常的處理程式可以從這個異常對象中得到所有資訊。
- 如果隻是把目前異常對象重新抛出,那麼printStackTrace()方法顯示的将是原來抛出點的調用棧資訊,而并非重新抛出點的資訊。要想更新這個資訊,可以調用fillInStackTrace()方法,這将傳回一個Throwable對象,它是通過把目前調用棧資訊填入原來那個異常對象而建立的。
- 如果是在捕獲異常之後抛出另一個異常,這麼做類似于使用fillInStackTrace(),有關原來異常發生點的資訊會丢失,剩下的是與新的抛出點有關的資訊。永遠不必為清理前一個異常對象而擔心,或者說為異常對象的清理而擔心。他們都是用new在堆上建立的對象,是以垃圾回收器會自動把它們清理掉。
- 異常鍊(異常轉譯):常常會想要在捕獲一個異常後抛出另一個異常,并且希望把原始異常的資訊儲存下來,這稱為異常鍊(異常轉譯)。
- 異常處理規則:異常處理的一個重要原則是隻有在你知道如何處理的情況下才捕獲異常,異常處理的一個重要目标就是把錯誤處理的代碼同錯誤發生的地點相隔離,這使你能在一段代碼中專注于要完成的事情,至于如何處理錯誤,則放在另一段代碼中完成。這樣主幹代碼就不會與錯誤處理邏輯混在一起,也更容易了解和維護。
- 不要過度的使用異常,會減慢程式的運作速度
- 不要用異常代替流程控制,代價高昂
- 不要将過大的内容包括在try塊中
- 不要忽略捕獲到的異常
性能問題
- 異常應該僅僅發生在異常情況下。當設計方法時,抛出異常不應該是方法傳回結果的标準方式。例如,如果檔案沒有被發現,那麼編寫檢查檔案存在的方法時可以傳回一個異常。但是如果檔案總是不存在,那麼此方法最好傳回布爾值
- 案例
-
大部分時間都是抛出異常,并且沒有覆寫method1()
fillInStackTrace()
-
雖然抛出異常但是覆寫method2()
來提高性能fillInStackTrace()
-
從來都不抛出異常。method3()
- 在
下多次調用100w次MacBook Pro 16G記憶體 i7 4核 jdk8
-
執行的時間大概在method1()
650ms-750ms
-
執行的時間大概在method2()
60ms-90ms
-
執行的時間大概在method3()
。3ms~4ms
- 這樣巨大的差别表明java中的異常對性能還是有很大影響的。如果必須使用異常,最好覆寫
(如果不關心異常棧的情況下,比如參數校驗場景抛異常,完全不需要關心異常棧資訊)可以提高性能Throwable.fillInStackTrace
-
public class ExceptionTest { public static void main(String[] args) { long startTime1 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { try { method1(i); } catch (Exception e) { // } } long endTime1 = System.currentTimeMillis(); //672ms 733ms 658ms System.out.println(endTime1 - startTime1); long startTime2 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { try { method2(i); } catch (Exception e) { // } } long endTime2 = System.currentTimeMillis(); //72ms 80ms 91ms System.out.println(endTime2 - startTime2); long startTime3 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { method3(i); } long endTime3 = System.currentTimeMillis(); //3ms 3ms 3ms System.out.println(endTime3 - startTime3); } public static boolean method1(int param1) { if (param1 > 1000000) return false; throw new BusinessException0(param1 + "抛出異常"); } public static boolean method2(int param2) { if (param2 > 1000000) return false; throw new BusinessException1(param2 + "抛出異常"); } public static boolean method3(int param3) { if (param3 > 1000000) return false; return true; } static class BusinessException0 extends RuntimeException { public BusinessException0(String message) { super(message); } } static class BusinessException1 extends RuntimeException { public BusinessException1(String message) { super(message); } @Override public synchronized Throwable fillInStackTrace() { //傳回null或者this return this; } } }
-
- jdk7裡
類新增了一個構造方法,可以動态決定是否需要異常棧。代碼如下,Throwable
參數的動态設定可以決定是否需要執行writableStackTrace
fillInStackTrace()
protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { if (writableStackTrace) { fillInStackTrace(); } else { stackTrace = null; } detailMessage = message; this.cause = cause; if (!enableSuppression) suppressedExceptions = null; }
異常聲明
- 如果方法裡的代碼産生了異常(調用其他聲明異常的方法)卻沒有進行處理,編譯器會發現這個問題并提醒你:要麼處理這個異常,要麼在本方法聲明将抛出的異常(比如throws Exception)。不過有一個能作弊的地方:可以聲明方法将抛出異常,實際上卻不抛出。編譯器相信了這個聲明,并強制此方法的使用者像真的抛出異常那樣使用這個方法。這樣做的好處是,為異常先占個位子,以後就可以抛出這種異常而不用修改已有的代碼。在定義抽象基類和接口時這種能力很重要,這樣派生類或接口實作就能夠抛出這些預先聲明的異常。
// 聲明異常但是方法中沒有異常代碼 public static boolean method4(int param4) throws Exception { if (param4 > 1000000) return false; return true; } public static void main(String[] args) { try { method4(1); } catch (Exception e) { e.printStackTrace(); } }
異常問題
- 案例解析:Java語言規範規定,如果一個catch子句要捕獲一個類型為E的受檢查異常,而其對應的try子句不能抛出E的某種子類型的異常,那麼這就是一個編譯期錯誤。但是捕獲Exception或Throwable的catch子句是合法的,不管與其對應的try子句的内容是什麼
1. 無法編譯通過 public class ExceptionTest2 { public static void main(String[] args) { try { System.out.println("hello world!"); } catch (IOException e) { System.out.println(e); } } } 2. 可以編譯通過,運作時不會輸出異常資訊 public class ExceptionTest2 { public static void main(String[] args) { try{ System.out.println("sdf"); }catch(Exception e){ System.out.println(e); } } }
- 案例分析:每一個接口都限制了方法f可以抛出的受檢查異常集合。一個方法可以抛出的受檢查異常集合是它所适用的所有類型聲明要抛出的受檢查異常集合的交集,而不是并集。是以Type3的對象類型上f方法根本不能抛出任何受檢查異常
interface Type1 { void f() throws CloneNotSupportedException; } interface Type2 { void f() throws InterruptedException; } interface Type3 extends Type1, Type2 { } public class ExceptionType implements Type3 { public static void main(String[] args) { ExceptionType exceptionTest = new ExceptionType(); //可以編譯通過,并列印hello world exceptionTest.f(); } @Override public void f() { System.out.println("hello world"); } }
- 構造器中的異常(陷阱)
- 有一點很重要,即你要時刻詢問自己"如果異常發生了,所有東西都被正确的清理嗎?"盡管大多是情況下是非常安全的,但涉及到構造器時,問題就出現了。如果在構造器内使用了異常 ,這些清理行為也許就不能正常工作了。這意味着在編寫構造器的時候要格外小心。也許你認為使用finally就可以解決問題了,但問題并非如此簡單,因為finally會每次都執行清理代碼,如果構造器在其執行過程中半途而廢,也許該對象的某些部分還沒有被建立成功,而這些部分在finally子句中卻是要清理的。
- 對于構造階段可能會抛出異常,并且要求清理的類,最安全的使用方式是使用嵌套的try子句。
- 在釋放多個IO資源時,都會抛出IOException ,于是可能為了省事如此寫
public static void inputToOutput(InputStream is, OutputStream os,boolean isClose) throws IOException { BufferedInputStream bis = new BufferedInputStream(is, 1024); BufferedOutputStream bos = new BufferedOutputStream(os, 1024); if (isClose) { bos.close(); bis.close(); } } 假設bos關閉失敗,bis還能關閉嗎?當然不能! 解決辦法:雖然抛出的是同一個異常,但是還是各自捕獲各的為好。否則第一個失敗,後一個面就沒有機會去釋放資源了.
異常的缺陷
- 異常作為程式出錯的标志,絕不應該被忽略,但它還是有可能被輕易地忽略。在用某些特殊的方式使用finally子句,就會發生這種情況。
class VeryImportantException extends Exception { @Override public String toString() { return "A very important exception!"; } } //hohum令人厭煩的 class HoHumException extends Exception { @Override public String toString() { //trivial 沒有價值的 return "A trivial exception"; } } public class LostMessage { void veryImportant() throws VeryImportantException { throw new VeryImportantException(); } void dispose() throws HoHumException { throw new HoHumException(); } /* * Output: A trivial exception * VeryImportantException不見了,它被finally子句裡的HoHumException所取代 */ public static void main(String[] args) { try { LostMessage lm = new LostMessage(); try { lm.veryImportant(); } finally { lm.dispose(); } } catch (Exception e) { System.out.println(e); } } }
- 從上面輸出中可以看到
不見了,它被finally子句裡的VeryImportantException
所取代。這是相當嚴重的缺陷HoHumException
- 最簡單的異常丢失
1. 如果運作這個程式,就會看到即使抛出了異常,它也不會産生任何的輸出. public class LostException { //什麼都不會輸出 public static void main(String[] args) { try { throw new RuntimeException(); } finally { return; } } }
JDK7異常新特性
- JDK7新增
- 捕獲多種異常類型
- 重新抛出異常
- 簡化資源清理
- 當在catch中聲明多種異常時,被聲明的異常預設為final的,也就是說不能再修改異常的引用。用一個catch處理多個異常,比用多個catch每個處理一個異常生成的位元組碼要更小更高效。
try { BufferedReader reader = new BufferedReader(new FileReader("")); Connection con = null; Statement stmt = con.createStatement(); } catch (IOException | SQLException e) { //捕獲多個異常,e就是final類型的 e.printStackTrace(); }
- try後面的圓括号裡隻能聲明并建立可自動關閉的資源,自動關閉資源時資源必須在try後的()中聲明,不能在try外聲明如下是錯誤的
BufferedReader reader = null; try(//關閉資源的代碼) { }catch{}
- 在Java SE 7及以後版本中,當你在catch語句裡聲明了一個或多個異常類型,并且在catch塊裡重新抛出了這些異常,編譯器根據下面幾個條件來去核實異常的類型。
- Try塊裡抛出它
- 前面沒有catch塊處理它
- 它是catch裡一個異常類型的父類或子類
-
是AutoCloseable的子接口Closeable
- 可以被自動關閉的資源類要麼實作AutoCloseable接口,要麼實作Closeable接口。java7幾乎把所有的資源類都實作了AutoCloseable或Closeable接口
- Closeable接口裡的close()方法聲明抛出了IOException異常,是以它的實作類在實作close()方法時隻能聲明抛出IOException或其子類
- AutoCloseable接口裡的close()方法聲明抛出了Exception,是以它的實作在實作close()方法時可以聲明抛出任何異常
抛出多個異常
- 一個方法抛出多個異常的案例:web界面注冊時,展現層依次把User對象傳遞給邏輯層,注冊方法需要對各個Field進行校檢并注冊,例如使用者名不能重複、密碼必須符合某種政策等,不要出現使用者第一次送出時系統提示"使用者名不能重複",在使用者修改使用者名後再次送出後,系統有提示密碼長度不能少于6位的情況,這個操作模式下的使用者體驗非常糟糕,最好的解決辦法就是封裝異常,建立異常容器,一次性地針對User對象進行校檢,然後傳回所有的異常。
public class ThrowMulitExceptionTest { public static void calc() throws ExceptionCollection { List<Throwable> list = new ArrayList<Throwable>(); try { int i = 1 / 0; System.out.println(i); } catch (Exception e) { list.add(e); } try { Integer j = null; j.intValue(); } catch (Exception e) { list.add(e); } //檢查是否有必要抛出異常 if (!list.isEmpty()) { throw new ExceptionCollection(list); } } } /** *MyExceptionCollection隻是一個異常容器, *可以容納多個異常,它本身并不代表任何異常含義 *所解決的是一次抛出多個異常 * */ class ExceptionCollection extends Exception { private List<Throwable> causes = new ArrayList<>(); public ExceptionCollection(List<? extends Throwable> e) { causes.addAll(e); } public List<Throwable> getException() { return causes; } }
無法捕獲的異常
- 是否可以寫一段Java代碼讓一個假設的
無法被捕獲?java.lang.ChuckNorrisException
- 你可以編譯一段代碼抛出一個
,但是在運作時動态生成一個并不繼承于Throwable接口的ChuckNorrisException
類。當然,為了讓這個過程可以進行,你需要關閉掉位元組碼驗證(ChuckNorrisException
)-Xverify:none
- 解決方案
- 你可以編譯一段代碼抛出一個
- 建立類
package cn.jannal.java.exception.notry; public class ChuckNorrisException extends RuntimeException //在第二次編譯時注釋此行 { public ChuckNorrisException() { } } package cn.jannal.java.exception.notry; public class TestVillain { public static void main(String[] args) { try { throw new ChuckNorrisException(); } catch (Throwable t) { System.out.println("Exception!"); } finally { System.out.println("Finally!"); } } }
- 第一步編譯
javac -cp . TestVillain.java ChuckNorrisException.java javac -cp cn.jannal.java.exception.notry TestVillain.java ChuckNorrisException.java javac -d . TestVillain.java ChuckNorrisException.java
- 第二步運作
$ java cn.jannal.java.exception.notry.TestVillain Exception! Finally!
- 第三步:注釋代碼并僅僅重新編譯
檔案ChuckNorrisException.java
public class ChuckNorrisException // extends RuntimeException //在第二次編譯時注釋此行 { public ChuckNorrisException() { } } $ javac -d . ChuckNorrisException.java
- 第四步:再次運作
$ java -Xverify:none cn.jannal.java.exception.notry.TestVillain Finally! Exception in thread "main" Exception: java.lang.AbstractMethodError thrown from the UncaughtExceptionHandler in thread "main"