無論人類的思惟有多麼縝密,也存在「智者千慮必有一失」的缺憾。不管計算機技術怎麼發展,也不可能窮盡全部的場景,這個世界是不完美的,是有缺陷的,完美的世界只存在於理想中。java
對於軟件帝國的締造者來講,程序也是不完美的,異常狀況會隨時出現,咱們須要它爲咱們描述例外時間,須要它處理非預期的情景,須要它幫咱們創建「完美世界」。數據庫
前言:淺談Java異常編程
一、在Java中,全部的異常都有一個共同的祖先Throwable(可拋出)。緩存
Throwable有兩個子類:Exception和error。安全
Trowable類中經常使用方法以下:函數
1. 返回異常發生時的詳細信息
public string getMessage();
2. 返回異常發生時的簡要描述
public string toString();
3. 返回異常對象的本地化信息。使用Throwable的子類覆蓋這個方法,能夠聲稱本地化信息。若是子類沒有覆蓋該方法,則該方法返回的信息與getMessage()返回的結果相同
public string getLocalizedMessage();
4. 在控制檯上打印Throwable對象封裝的異常信息
public void printStackTrace();性能
二、異常分兩大類:測試
①運行時異常:都是RuntimeException類及其子類異常,如NullPointerException(空指針異常)、IndexOutOfBoundsException(下標越界異常)等,這些異常是不可查異常,這些異常通常由程序邏輯錯誤引發的,程序應該從邏輯角度儘量避免這些異常的發生。運行時異常的特色是Java編譯器不會檢查它,也就是說,當程序中可能出現這類異常,即便沒有用try-catch語句捕獲它,也沒有用throws子句聲明拋出它,也會編譯經過。優化
Java的異常(Throwable)分爲可查的異常(checked exceptions)和不可查的異常(unchecked exceptions)。this
① 可查異常(編譯器要求必須處置的異常):正確的程序在運行中,很容易出現的、情理可容的異常情況。除了Exception中的RuntimeException及其子類之外,其餘的Exception類及其子類(例如:IOException和ClassNotFoundException)都屬於可查異常。這種異常的特色是Java編譯器會檢查它,也就是說,當程序中可能出現這類異常,要麼用try-catch語句捕獲它,要麼用throws子句聲明拋出它,不然編譯不會經過。
② 不可查異常(編譯器不要求強制處置的異常):包括運行時異常(RuntimeException與其子類)和錯誤(Error)。
三、異常處理的機制
① 拋出異常:任何Java代碼均可以拋出異常。
② 捕獲異常:捕捉異常經過try-catch語句或者try-catch-finally語句實現。
finally 塊:不管是否捕獲或處理異常,finally塊裏的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回以前被執行。在如下4種特殊狀況下,finally塊不會被執行:
1)在finally語句塊中發生了異常。
2)在前面的代碼中用了System.exit()退出程序。
3)程序所在的線程死亡。
4)關閉CPU。
應該在聲明方法拋出異常仍是在方法中捕獲異常?
捕捉並處理知道如何處理的異常,而拋出不知道如何處理的異常。
整體來講,Java規定:對於可查異常必須捕捉、或者聲明拋出。容許忽略不可查的RuntimeException和Error。
Java中使用異經常見的問題
建議110:提倡異常封裝
建議111:採用異常鏈傳遞異常
建議112:可查異常儘量轉化爲不可查異常
建議113:不要在finally中處理返回值
建議114:不要在構造函數中拋出異常
建議115:使用Throwable得到棧信息
建議116:異常只爲異常服務
建議117:多使用異常,把性能問題放一邊
建議110:提倡異常封裝
Java語言的異常處理機制能夠確保程序的健壯性,提升系統的可用率,可是Java API提供的異常都是比較低級別的,只有開發人員才能看的懂。而對於終端用戶來講,這些異常無異於天書,那該怎麼辦呢?這就須要咱們對異常進行封裝。
異常封裝有三方面的有點:
一、提升系統的友好性
二、提升系統的可維護性
正確的作法是對異常進行分類處理,並進行封裝輸出,代碼以下:
public void doStuff4(){ try{ //doSomething }catch(FileNotFoundException e){ log.info("文件未找到,使用默認配置文件...."); e.printStackTrace(); }catch(SecurityException e1){ log.info(" 無權訪問,可能緣由是......"); e1.printStackTrace(); } }
如此包裝後,維護人員看到這樣的異常就有了初步的判斷,或者檢查配置,或者初始化環境,不須要直接到代碼層級去分析了。
三、解決Java異常機制自身的缺陷
Java中的異常一次只能拋出一次,好比doStuff方法中有兩個邏輯代碼片斷,若是在第一個邏輯片斷中拋出異常,則第二個邏輯片斷就再也不執行了,也就沒法拋出第二個異常了,如今的問題是如何才能一次拋出兩個或更多的異常呢?
其實,使用自行封裝的異常能夠解決該問題,代碼以下:
class MyException extends Exception { // 容納全部的異常 private List<Throwable> causes = new ArrayList<Throwable>(); // 構造函數,傳遞一個異常列表 public MyException(List<? extends Throwable> _causes) { causes.addAll(_causes); } // 讀取全部的異常 public List<Throwable> getExceptions() { return causes; } }
MyException異常只是一個異常容器,能夠容納多個異常,但它自己並不表明任何異常含義,它所解決的是一次拋出多個異常的問題,具體調用以下:
public void doStuff() throws MyException { List<Throwable> list = new ArrayList<Throwable>(); // 第一個邏輯片斷 try { // Do Something } catch (Exception e) { list.add(e); } // 第二個邏輯片斷 try { // Do Something } catch (Exception e) { list.add(e); } // 檢查是否有必要拋出異常 if (list.size() > 0) { throw new MyException(list); } }
這樣一來,DoStuff方法的調用者就能夠一次得到多個異常了,也可以爲用戶提供完整的例外狀況說明。可能有人會問:這種狀況會出現嗎?怎麼回要求一個方法拋出多個異常呢?
絕對有可能出現,例如Web界面註冊時,展示層依次把User對象傳遞到邏輯層,Register方法須要對各個Field進行校驗並註冊,例如用戶名不能重複,密碼必須符合密碼策略等,不要出現用戶第一次提交時系統顯示" 用戶名重複 ",在用戶修改用戶名再次提交後,系統又提示" 密碼長度小於6位 " 的狀況,這種操做模式下的用戶體驗很是糟糕,最好的解決辦法就是異常封裝,創建異常容器,一次性地對User對象進行校驗,而後返回全部的異常。
建議111:採用異常鏈傳遞異常
正確的作法是先封裝再傳遞,步驟以下:
好比咱們的JavaEE項目通常都有三層結構:持久層,邏輯層,展示層,持久層負責與數據庫交互,邏輯層負責業務邏輯的實現,展示層負責UI數據庫的處理。
一、把FIleNotFoundException封裝爲MyException。
二、拋出到邏輯層,邏輯層根據異常代碼(或者自定義的異常類型)肯定後續處理邏輯,而後拋出到展示層。
三、展示層自行決定要展示什麼,若是是管理員則能夠展示低層級的異常,若是是普通用戶則展現封裝後的異常。
在IOException的構造函數中,上一個層級的異常能夠經過異常鏈進行傳遞,鏈中傳遞異常的代碼以下所示:
try{ //doSomething }catch(Exception e){ throw new IOException(e); }
捕捉到Exception異常,而後將其轉化爲IOException異常並拋出(此方法叫異常轉譯),調用者得到該異常後再調用getCause方法便可得到Exception的異常信息。
綜上所述,異常須要封裝和傳遞,咱們在開發時不要「吞噬」異常,也不要赤裸裸的拋出異常,封裝後再拋出,或者經過異常鏈傳遞,能夠達到系統更健壯,更友好的目的。
建議112:可查異常儘量轉化爲不可查異常
可查異常(Checked Exception)是正常邏輯的一種補償手段,特別是對可靠性要求比較高的系統來講,在某些條件下必須拋出可查異常以便由程序進行補償處理,也就是說可查異常有存在的理由,那爲何要把可查異常轉化爲非=不可查異常呢?可查異常確實有不足的地方:
一、可查異常使接口聲明脆弱
咱們要儘可能多使用接口編程,能夠提升代碼的擴展性、穩定性,可是涉及異常問題就不同了,例如系統初期是一個接口是這樣設計的:
interface User{ //修改用戶密碼,拋出安全異常 public void changePassword() throws MySecurityException; }
可能有多個實現者,也可能拋出不一樣的異常。
這裏會產生兩個問題:① 異常時主邏輯的補充邏輯,修改一個補充邏輯,就會致使主邏輯也被修改,也就會出現實現類「逆影響」接口的情景,咱們知道實現類是不穩定的,而接口是穩定的,一旦定義異常,則增長了接口的不穩定性,這是面向對象設計的嚴重褻瀆;② 實現的變動最終會影響到調用者,破壞了封裝性,這也是迪米特法則鎖不能容忍的。
迪米特法則,俗稱最少知識法則,就是說,一個對象應當對其它對象有儘量少的瞭解,儘可能下降類與類之間的耦合度。
迪米特法則的初衷是下降類之間的耦合,因爲每一個類都減小了沒必要要的依賴,所以的確能夠下降耦合關係。可是凡事都要有度,雖然能夠避免與非直接的類通訊,可是要通訊,必然會經過一個「中介」來發生聯繫,過度的使用迪米特原則,會產生大量這樣的中介和傳遞類,致使系統的複雜度變大。因此在採用迪米特原則的時間,要反覆權衡,既作到結構清晰,又要高內聚低耦合。
在將迪米特法則運用到系統的設計中時,應注意的幾點:
① 在類的劃分上,應該建立弱耦合的類;
② 在類的結構設計上,每一個類都應該儘可能下降類的訪問權限、下降成員的訪問權限;
③ 在類的設計上,只要有可能,一個類應當設計成不變類;
④ 一個對象在對其它對象的引用應當下降到最低。
⑤ 謹慎使用序列化功能;
⑥ 不要暴露類成員,而應該提供相應的訪問器。
二、可查異常使代碼的可讀性下降
一個方法增長了可查異常,則必須有一個調用者對異常進行處理。
用try...catch捕捉異常,代碼膨脹不少,可讀性也就下降了,特別是多個異常須要捕捉的時候,並且可能在catch中再次拋出異常,這大大下降了代碼的可讀性。
三、可查異常增長了開發工做量
咱們知道異常須要封裝和傳遞,只有封裝才能讓異常更容易理解,上層模塊才能更好的處理,可這會致使低層級的異常沒完沒了的封裝,無故加劇了開發的工做量。
可查異常有這麼多的缺點,有什麼好的方法能夠避免或減小這些缺點呢?就是將可查異常轉化爲不可查異常,可是也不能把全部的異常轉化爲不可查異常,有不少的未知不肯定性。
咱們能夠在實現類中根據不一樣狀況拋出不一樣的異常,簡化了開發工做,提升了代碼的可讀性。
那什麼樣的能轉化,什麼樣的不能轉化呢?
當可查異常威脅到系統額安全性、穩定性、可靠性、正確性,則必須處理,不能轉化爲不可查異常,其它狀況便可轉化爲不可查異常。
建議113:不要在finally中處理返回值
一、覆蓋了try代碼塊中的return返回值
public static int doStuff() { int a = 1; try { return a; } catch (Exception e) { } finally { // 從新修改一下返回值 a = -1; } return 0; }
該方法的返回值永遠是1,不會是-1或0(爲何不會執行到" return 0 " 呢?緣由是finally執行完畢後該方法已經有返回值了,後續代碼就不會再執行了)
public static Person doStuffw() { Person person = new Person(); person.setName("張三"); try { return person; } catch (Exception e) { } finally { // 從新修改一下值 person.setName("李四"); } person.setName("王五"); return person; }
此方法的返回值永遠都是name爲李四的Person對象,緣由是Person是一個引用對象,在try代碼塊中的返回值是Person對象的地址,finally中再修改那固然會是李四了。
上面的兩個例子能夠好好的琢磨琢磨!
二、屏蔽異常
public static void doSomeThing(){ try{ //正常拋出異常 throw new RuntimeException(); }finally{ //告訴JVM:該方法正常返回 return; } } public static void main(String[] args) { try { doSomeThing(); } catch (RuntimeException e) { System.out.println("這裏是永遠不會到達的"); } }
上面finally代碼塊中的return已經告訴JVM:doSomething方法正常執行結束,沒有異常,因此main方法就不可能得到任何異常信息了。
這樣的代碼會使可讀性大大下降,讀者很難理解做者的意圖,增長了修改的難度。
與return語句類似,System.exit(0)或RunTime.getRunTime().exit(0)出如今異常代碼塊中也會產生很是多的錯誤假象,增長代碼的複雜性,你們有興趣能夠自行研究一下。
建議114:不要在構造函數中拋出異常
一、構造函數中拋出錯誤是程序猿沒法處理的
二、構造函數不該該拋出不可查異常
class Person { public Person(int _age) { // 不滿18歲的用戶對象不能創建 if (_age < 18) { throw new RuntimeException("年齡必須大於18歲."); } } public void doSomething() { System.out.println("doSomething......"); } }
public static void main(String[] args) { Person p = new Person(17); p.doSomething(); /*其它的業務邏輯*/ }
game over了!
三、構造函數中儘量不要拋出可查異常
① 致使子類膨脹
② 違背了里氏替換原則:「里氏替換原則」是說父類能出現的地方子類就能夠出現,並且將父類替換爲子類也不會產生任何異常。
//父類 class Base { // 父類拋出IOException public Base() throws IOException { throw new IOException(); } } //子類 class Sub extends Base { // 子類拋出Exception異常 public Sub() throws Exception { } }
Sub的構造函數拋出了Exception異常,它比父類的構造函數拋出更多的異常範圍要寬,必須增長新的catch塊才能解決。
在構造函數中拋出受檢異常會違背里氏替換原則原則,使咱們的程序缺少靈活性。
③ 子類構造函數擴展受限:子類存在的緣由就是指望實現擴展父類的邏輯,但父類構造函數拋出異常卻會讓子類構造函數的靈活性大大下降,例如咱們指望這樣的構造函數。
package OSChina.Throwable; import java.io.IOException; public class Base { // 父類拋出IOException public Base() throws IOException { throw new IOException(); } }
這就尷尬了!
受檢異常儘可能不拋出,能用曲線的方式實現就用曲線方式實現!
建議115:使用Throwable得到棧信息
AOP編程能夠很輕鬆的控制一個方法調用哪些類,也可以控制哪些方法容許被調用,通常來講切面編程,只能控制到方法級別,不能實現代碼級別低的植入(Weave)。
使用Throwable得到棧信息,而後鑑別調用者並分別輸出,代碼以下:
package OSChina.Throwable; public class Foo { public static boolean method(){ // 取得當前棧信息 StackTraceElement[] ste = new Throwable().getStackTrace(); //檢查是不是methodA方法調用 for(StackTraceElement st:ste){ if(st.getMethodName().equals("methodA")){ return true; } } return false; } }
package OSChina.Throwable; public class Invoker { //該方法打印出true public static void methodA(){ System.out.println("methodA(),"+Foo.method()); } //該方法打印出false public static void methodB(){ System.out.println("methodB(),"+Foo.method()); } public static void main(String[] args) { methodA(); methodB(); } }
注意看Invoker類,兩個方法methodA和methodB都調用了Foo的method方法,都是無參調用,返回值卻不一樣,這是咱們的Throwable類發揮效能了。JVM在建立一本Throwable類及其子類時會把當前線程的棧信息記錄下來,以便在輸出異常時準肯定位異常緣由,咱們來看Throwable源代碼。
public class Throwable implements Serializable { private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0]; //出現異常記錄的棧幀 private StackTraceElement[] stackTrace = UNASSIGNED_STACK; //默認構造函數 public Throwable() { //記錄棧幀 fillInStackTrace(); } //本地方法,抓取執行時的棧信息 private native Throwable fillInStackTrace(int dummy); public synchronized Throwable fillInStackTrace() { if (stackTrace != null || backtrace != null /* Out of protocol state */) { fillInStackTrace(0); stackTrace = UNASSIGNED_STACK; } return this; } }
在出現異常時(或主動聲明一個Throwable對象時),JVM會經過fillInStackTrace方法記錄下棧幀信息,而後生成一個Throwable對象,這樣咱們就能夠知道類間的調用順序,方法名稱及當前行號等了。
咱們雖然能夠根據調用者的不一樣產生不一樣的邏輯,但這僅侷限在對此方法的普遍認知上,更多的時候咱們使用method方法的變形體,代碼以下:
class Foo { public static boolean method() { // 取得當前棧信息 StackTraceElement[] sts = new Throwable().getStackTrace(); // 檢查是不是methodA方法調用 for (StackTraceElement st : sts) { if (st.getMethodName().equals("methodA")) { return true; } } throw new RuntimeException("除了methodA方法外,該方法不容許其它方法調用"); } }
只是把「return false」 替換成了一個運行期異常,除了methodA方法外,其它方法調用都會產生異常,該方法經常使用做離線註冊碼校驗,讓破解者視圖暴力破解時,因爲執行者不是指望的值,所以會返回一個通過包裝和混淆的異常信息,大大增長了破解難度。
建議116:異常只爲異常服務
異常只爲異常服務,這是何解?難道異常還能爲其它服務不成?確實能,異常本來是正常邏輯的一個補充,可是有時候會被當作主邏輯使用,看以下代碼:
//判斷一個枚舉是否包含String枚舉項 public static <T extends Enum<T>> boolean Contain(Class<T> clz,String name){ boolean result = false; try{ Enum.valueOf(clz, name); result = true; }catch(RuntimeException e){ //只要是拋出異常,則認爲不包含 } return result; }
判斷一個枚舉是否包含指定的枚舉項,這裏會根據valueOf方法是否拋出異常來進行判斷,若是拋出異常(通常是IllegalArgumentException異常),則認爲是不包含,若不拋出異常則能夠認爲包含該枚舉項,看上去這段代碼很正常,可是其中有是哪一個錯誤:
一、異常判斷下降了系統的性能
二、下降了代碼的可讀性,只有詳細瞭解valueOf方法的人才能讀懂這樣的代碼,由於valueOf會跑出一個不可查異常。
三、隱藏了運行期可能產生的錯誤,catch到異常,但沒有作任何處理。
// 判斷一個枚舉是否包含String枚舉項 public static <T extends Enum<T>> boolean Contain(Class<T> clz, String name) { // 遍歷枚舉項 for (T t : clz.getEnumConstants()) { // 枚舉項名稱是否相等 if (t.name().equals(name)) { return true; } } return false; }
常只能用在非正常的狀況下,不能成爲正常狀況下的主邏輯,也就是說,異常是是主邏輯的輔助場景,不能喧賓奪主。並且,異常雖然是描述例外事件的,但能避免則避免之,除非是確實沒法避免的異常,例如:
public static void main(String[] args) { File file = new File("a.txt"); try { FileInputStream fis = new FileInputStream(file); // 其它業務處理 } catch (FileNotFoundException e) { e.printStackTrace(); // 異常處理 } }
這樣一段代碼常常在咱們的項目中出現,但常常寫並不表明不可優化,這裏的異常類FileNotFoundException徹底能夠在它誕生前就消除掉:先判斷文件是否存在,而後再生成FileInputStream對象,這也是項目中常見的代碼:
public static void main(String[] args) { File file = new File("a.txt"); // 常常出現的異常,能夠先作判斷 if (file.exists() && !file.isDirectory()) { try { FileInputStream fis = new FileInputStream(file); // 其它業務處理 } catch (FileNotFoundException e) { e.printStackTrace(); // 異常處理 } } }
雖然增長了if判斷語句,增長了代碼量,可是卻減小了FileNotFoundException異常出現的概率,提升了程序的性能和穩定性。
建議117:多使用異常,把性能問題放一邊
咱們知道異常是主邏輯的例外邏輯,舉個簡單的例子來講,好比我在馬路上走(這是主邏輯),忽然開過一輛車,我要避讓(這是受檢異常,必須處理),繼續走着,忽然一架飛機從我頭頂飛過(非受檢異常),咱們能夠選在繼續行走(不捕捉),也能夠選擇指責其噪音污染(捕捉,主邏輯的補充處理),再繼續走着,忽然一顆流星砸下來,這沒有選擇,屬於錯誤,不能作任何處理。
使用異常還有不少優勢,可讓正常代碼和異常代碼分離、能快速查找問題(棧信息快照)等,可是異常有一個缺點:性能比較慢。
Java的異常機制缺失比較慢,這個「比較慢」是相對於String、Integer等對象而言,單單從建立對象來講,new一個IOException會比String慢5倍,這從異常的處理機制上能夠解釋:
由於new異常要執行fillInStackTrace方法,要記錄當前棧的快照,而String類則是直接申請一個內存建立對象,異常類慢半拍再說不免。
並且,異常類時不能緩存的。
難道異常的性能問題就沒有任何能夠提升的辦法了?確實沒有,可是咱們不能由於性能問題而放棄使用異常,並且通過測試,在JDK1.6下,一個異常對象的建立時間只需1.4毫秒左右(注意是毫秒,一般一個交易是在100毫秒左右),難道咱們的系統連如此微小的性能消耗都不予許嗎?
注意:性能問題不是拒絕異常的藉口。