有一句這樣話:一個衡量Java設計師水平和開發團隊紀律性的好方法就是讀讀他們應用程序裏的異常處理代碼。html
本文主要討論開發Java程序時,如何設計異常處理的代碼,如什麼時候拋異常,捕獲到了怎麼處理,而不是講異常處理的機制和原理。java
在我本身研究Java異常處理以前,我查過不少資料,翻過不少書藉,試過不少搜索引擎,換過不少英文和中文關鍵字,可是關於異常處理設計的文章實在太少,在我研究完Java異常處理以後,我面試過不少人,也問過不少老員工,極少碰到對Java異常有研究的人,看來研究這個主題的人不多,本文內容本是我的研究異常時作的筆記,現整理一下與你們一塊兒分享。程序員
首先咱們簡單的回顧一下基礎知識,Java中有兩種異常,嚴格的說是三種,包含四個類,層次圖以下:面試
捕獲到了編譯時異常怎麼處理:編程
這個話題恐怖是最古老的啦,網上的文章多數都是討論這個話題,但這些文章大部分只是給了幾條禁止的原則,他們是:1)不要直接忽略異常;2)不要用try-catch包住過多語句;3)不要用異常處理來處理程序的正常控制流;4)不要隨便將異常迎函數棧向上傳遞,能處理儘可能處理。他們都對,可是要作異常處理的設計,信息仍是不夠,好比第一條他只是告訴了不要忽略,但沒有告訴咱們怎麼處理,因此不少人直接e.printStackTrace()了,這種處理比直接忽略是好一點,但還不夠好。對於第二條,他的理由是避免耗資源很大,不過「過多語句」這句話描述的太模糊了,沒說明到底多少纔算過多,以至於不少人的try-catch語句只包住會拋編譯時異常的那一行代碼,若是一段代碼中有多行代碼會拋編譯時異常,那這一段代碼中可能有多個try-catch語句塊,像這樣:服務器
1 LLJTran llj = new LLJTran(file); 2 try { 3 llj.read(LLJTran.READ_INFO,true); 4 } catch (LLJTranException e) { 5 // ... 6 } 7 8 // ... 9 10 OutputStream out = null; 11 try { 12 File out = new File(file.getPath()+"_bak.jpg"); 13 llj.xferInfo(null, out, LLJTran.REPLACE, LLJTran.REPLACE); 14 } catch (IOException e) { 15 // ... 16 } 17 18 // ... 19 20 try { 21 out.close(); 22 } catch (IOException e) { 23 // ... 24 }
這樣有什麼壞處呢,處處都是異常處理的代碼,很容易給人形成困惑,很難找出哪些是正常流程的代碼,並且還違背了Java異常機制的初衷,Java異常機制是爲了把異常處理的代碼與正常流程的代碼分開,避免程序中出現過多的像傳統程序那樣的非法值判斷語句,以至於擾亂了正常流程。但上述代段充斥着try-catch語句塊,已經擾亂了主流程,並極大影響了可讀性。架構
try-catch既不能包太多代碼,又不能包太少,那應該包多少才適合呢,這個問題我查過的資料中都沒有提,個人我的建議是包住邏輯關係緊密的代碼,好比打開文件,讀取文件,關閉文件,我認爲就是邏輯關係緊密的代碼,若是你發現包住的代碼不少,能夠封裝一些方法,如讀取文件的代碼很長就應該封裝成一個方法,這個方法能夠申明IOException,(其實讀文件的細節原本屬於低層邏輯,打開,讀取,關閉才屬於同層邏輯,若是讀取代碼很短,初期爲了省事纔不封成讀取細節的代碼,不事後期能夠重構並封裝成方法,這是《重構·改善繼有的代碼設計》一書中的思想——軟件應該不斷的重構和加善)。這樣才能達到把異常代碼與正常流程代碼分離的目的。oracle
第3)條沒問題,第4)條也有問題,「不要隨便」很模糊,那何時才能向上傳遞呢。ide
吐槽完了,咱們如今來講說到底該如何處理捕獲到的編譯時異常:函數
1、恢復並繼續執行:這個結果是最完美的,也是編譯時異常出生的目的——捕獲異常,並恢復繼續執行程序。因此若是你捕獲了一個異常是先盡力恢復,這種狀況其實就是在主方案行不通時,用備選方案,並且主方案可否行通不能事先知道,必須執行的時候才能知道,因此在通常狀況下,備選方案比主方案要的運行結果要差。好比一個視頻程序,它要調用一個下載節目列表的方法,可能以下:
1 InputStream download() throws IOException { 2 // ... 3 }
但服務器不保證老是可用,有可能被攻擊了,有可能其它緣由,由於是個意外事件,因此又不可能事先知道,因而異常就發生在執行過程當中,幸虧客戶端有備選方案,它在本地保存了一個默認列表,當服務器不可用時,就加載本地列表,因此客戶端對這個異常的處理能夠以下:
1 public void loadProgramList() { 2 InputStream inputStream; 3 try { 4 inputStream = download(); 5 } catch (IOException e) { 6 // Log this exception 7 System.out.println("The server occurred errors"); 8 // Use the local file 9 inputStream = openLocalFile(); 10 } 11 12 //... 13 } 14 15 private InputStream download() throws IOException { 16 // ... 17 } 18 19 private InputStream openLocalFile() { 20 // ... 21 }
惋惜的是,不是任什麼時候候的異常均可以恢復,反而通常狀況是不能恢復的。
2、向上傳播異常:向上傳播就是在本方法上用throws申明,本方法裏的代碼不對某異常作任何處理。若是不能用上述恢復措施,就檢查能不能向上傳播,什麼狀況下能夠向上傳播呢?有多種說法,一種說法是當本方法恢復不了時,這個說法顯然是錯誤,由於上層也不必定能恢復。另外還有兩種說法是:1.當上層邏輯能夠恢復程序時;2.當本方法除了打印以外不能作任何處理,並且不肯定上層可否處理。這種兩種說法都是正確的,但還不夠,由於也有的狀況,明確知道上層恢復不了也須要上層處理,因此我認爲正確的作法是:當你認爲本異常應該由上層處理時,才向上傳播。不過這得根據你程序的設計來靈活思考,好比你的類設計了一個上層方法集中處理異常,而下層有一些private方法只是簡單的用throws申明。當上層方法捕獲到異常時,雖然不能恢復執行,但能夠作一些處理,如轉換成便於閱讀的文本,或者用下面討論的轉譯。
3、轉譯異常:轉譯即把低層邏輯的異常轉化成爲高層邏輯的異常,由於有可能低層邏輯的異常在高層邏輯中不能被理解,主要實現是新寫一個Exception的子類,而後在低層邏輯捕獲異常,改拋這個新寫的異常,好比剛剛那個視頻程序,他的主流程多是:1.加載節目列表,2.顯示播放節目。而加載節目列表子流程又包含讀取節目文件、解析節目文件、顯示節目列表。而讀取節目文件有可能出現IO異常(有可能本地和網上的文件都讀不了了),解析節目文件可能出現解析異常,這時若是把這些異常,直接向上傳播,變成這樣,你以爲合理嗎:
1 public void mainFlow() { 2 // 1.load program list 3 try { 4 loadProgramList(); 5 } catch (IOException e) { 6 // I don't understand what is this exception. 7 } catch (ParseException e) { 8 // I don't understand what is this exception. 9 } 10 11 // 2.play program 12 // ... 13 } 14 15 public void loadProgramList() throws IOException, ParseException { 16 // 1.Read program file 17 InputStream inputStream; 18 try { 19 inputStream = download(); 20 } catch (IOException e) { 21 // Log this exception 22 System.out.println("The server occurred errors"); 23 // Use the local file 24 inputStream = openLocalFile(); //Maybe throw IOException. 25 } 26 27 // 2.Parse program file 28 parserProgramFile(inputStream); //Maybe throw ParseException. 29 30 // 3.Display program file 31 //... 32 }
因爲loadProgramList將兩個可能的異常向上傳播,在mainFlow裏,必須顯示捕獲這兩個異常,但在mainFlow根本就不能理解這兩個異常表明什麼,mainFlow裏只須要知道加載節目列表異常就能夠了,因此咱們能夠寫一個異常類LoadProgramException表明加載節目異常,並在loadProgramList裏拋出,因而代碼變成這樣:
1 public void mainFlow() { 2 // 1.load program list 3 try { 4 loadProgramList(); 5 } catch (LoadProgramException e) { // look at here 6 // ... 7 } 8 9 // 2.play program 10 // ... 11 } 12 13 public void loadProgramList() throws LoadProgramException { // look at here 14 // 1.Read program file 15 InputStream inputStream = null; 16 try { 17 inputStream = download(); 18 } catch (IOException e) { 19 // Log this exception 20 System.out.println("The server occurred errors"); 21 // Use the local file 22 try { 23 inputStream = openLocalFile(); 24 } catch (IOException e1) { 25 throw new LoadProgramException("Read program file error.", e1); // look at here 26 } 27 } 28 29 // 2.Parse program file 30 try { 31 parserProgramFile(inputStream); 32 } catch (ParseException e) { 33 throw new LoadProgramException("Parse program file error.", e); // look at here 34 } 35 36 // 3.Display program file 37 //... 38 } 39 40 // ... 41 42 class LoadProgramException extends Exception { 43 public LoadProgramException(String msg, Throwable cause) { 44 super(msg, cause); 45 } 46 // ... 47 }
注意:LoadProgramException構造函數的第一個參數是表明緣由,用於組成異常鏈,異常鏈是一種機制,異常轉譯時,保存原來的異常,這樣當這個異常再被轉譯時,還會被保存,因而就成了一條鏈了,包含了全部的異常,因此你能夠看到這樣的異常打印:
Exception in thread "main" java.lang.NoClassDefFoundError: graphics/shapes/Square at Main.main(Main.java:7) Caused by: java.lang.ClassNotFoundException: graphics.shapes.Square at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 1 more
這個異常鏈中就是包含了兩個異常,最前面是頂級異常,後面再打印一個Cause by,而後再打印低一層異常,直到打印完全部的異常。
另外,主流程中還有一個播放流程也能夠定義一個播放異常的類,再作這樣的轉譯處理,可是,若是流程多,是否是得寫多個異常類呢,有人建議是每一個包定義一個異常類,但並非絕對的,這個細粒度還要根據具體的程序邏輯來決定,這種把握能力就要靠經驗了,這可能就是架構師的過人之處了。
4、改拋爲運行時異常:這個很好玩,也是一條很方便的處理手法(我經常使用,我用這個還發現了一個Android系統的bug),即當你捕獲到異常時,從新拋出,這跟轉譯很類似,有一點區別,這裏拋的是運行時異常,而轉譯拋的是編譯時異常。那何時使用這個手法呢?簡單的說就是當某個異常出現時,你必須讓程序掛掉。解釋一下:若是某個異常狀況一旦出現,程序便沒法繼續執行,並且你明確知道本方法和上層邏輯作不出任何有意義的處理,你只能讓程序退出。因此你就拋一個運行時異常讓程序掛掉。舉個例子,好比在加密通訊中,服務器捕獲到了一個非法數據異常,這是沒法恢復的,並且就是拋一運行異常,讓線程掛掉,鏈接便會自動中斷。
5、記錄並消耗掉異常:這個手法就是把異常記錄下來(到文件或控制檯)而後忽略掉異常,有可能隨後就讓本方法返回null,這個手法通常用在不是很嚴重的異常,至關因而warning級別的錯誤,出現這個異常對程序的執行可能影響不太,好比程序的某個偏好設置文件(如窗口位置,最近文件等)損壞,但這個文件信息不多,程序只要使用默認配置便可。
有不必顯示捕獲運行時異常:
運行時異常通常是不須要捕獲的,由於它的目的就是讓程序在沒法恢復時掛掉,可是也有特殊需求,好比你要收集全部的未捕獲異常記錄,可能用於統計,也可能用於未來調試。還有其它緣由使你不想讓程序直接掛掉,好比你想把友好信息告訴用戶。
何時須要拋異常:
立刻就要討論如何拋異常了,但在必須先知道,何時須要拋異常,簡單的說就是遇到一個異常狀況,這是一個模棱兩可的問題,就像美不美這個問題同樣,我幾種說法,你看你能理解哪種,一種是正常狀況的反面,即非正常狀況,那什麼是非正常狀況呢,這也是仁者見仁,智者見智,好比說讀到文件尾,這個算正常仍是異常呢,都說得過去,因此這裏給一個判斷方法作爲參考,若是是一個典型狀況,就不當成是異常,因此讀到文件尾就沒有被當成一個異常,返回了-1。還有一種說法是,程序執行的必要條件不能成立,使得本方法沒法繼續履行本身的職責。這兩種說法都不錯,你均可以用,並且覆蓋了大部分狀況。
什麼時候選用編譯時異常:編譯時異常是Java特有的,其它語言沒有,剛出來時很流行,因此你能夠看到流處理包裏充斥着IOException,但通過多年的使用,有人以爲編譯時異常是一種實驗性錯誤,應該徹底丟棄,說這個話的人就是《Think In Java》的一書的做者Eckel,我認爲這種說法太絕對了,關於這個是與否也有很大的爭論。《Effective Java》一書的做者則認爲應避免沒必要要的編譯時異常,由於你拋編譯時異常會給強制要求調用者捕獲,這會增長他的負擔,我是這一觀點的支持者。那到底什麼時候拋編譯時異常呢?當你發現一個異常狀況時,檢查這兩個條件,爲真時選用編譯時異常:1、若是調用者能夠恢復此異常狀況,2、若是調用者不能恢復,但能作出有意義的事,如轉譯等。若是你不肯定調用者可否作出有意義的事,就別使編譯時異常,省得被抱怨。還有一條原則,應盡最大可能使用編譯時異常來代替錯誤碼,這條也是編譯時異常設計的目的。另外,必須注意使用編譯時異常的目的是爲了恢復執行,因此設計異常類的時候,應提供儘可能多的異常數據,以便於上層恢復,好比一個解析錯誤,能夠在設計的異常類寫幾個變量來存儲異常數據:解析出錯的句子的內容,解析出錯句子的行號,解析出錯的字符在行中的位置。這些信息可能幫助調用恢復程序。
什麼時候選用運行時異常:首先,運行時異常確定是不可恢復的異常,不然按上段方法處理。這個不可恢復指的是運行時期不可恢復,若是能夠修改源代碼來避免本異常的發生呢,那說明這是一個編程錯誤,對於編程錯誤,必定要拋運行時異常,編程錯誤通常能夠經過修改代碼來永久性避免該異常,因此這種狀況應該讓程序掛掉,至關於爆出一個bug,從而提醒程序員修改代碼。這種編程錯誤能夠總結一下,API是調用者與實現者之間的契約,調用者必須遵照契約,好比傳入的參數不容許爲空,這一點是隱含契約,不必明確寫出來的,若是違反契約,實現者就能夠拋運行時異常,讓程序掛掉以提醒調用者。
其它狀況是否應使用運行時異常,上面提到過,就是誰都無能爲力的異常狀況,還有就是你不肯定到底能不能恢復,除此以外,你能夠這樣判斷:若是你但願程序掛掉,就用運行時異常。須要說明的是,請儘可能使用系統自帶異常,而不是新寫。網上還有一條建議是使用運行時異常時, 必定要將全部可能的異常寫進文檔。這認爲只要把不經常使用的寫上便可,像NullPointException每一個方法都有可能拋,但不必每一個方法都寫說明。
將編譯時異常重構成運行時異常:
你可能手頭上有一份之前的代碼,大量的使有了編譯時異常,但不少都是沒有必要的編譯時異常,致使調用上不方便,《Effective Java》裏有一種方法能夠將編譯時異常轉爲運行時異常:將原來拋編譯時異常的方法,拆成兩個方法,其中一個是用來指示異常是否爲發生,即將如下代碼:
// Invocation with checked exception try { obj.action(args); } catch(TheCheckedException e) { // Handle exceptional condition ... }
改成這樣:
// Invocation with state-testing method and unchecked exception if (obj.actionPermitted(args)) { obj.action(args); } else { // Handle exceptional condition ... }
步驟是:1)將原來方法foo的異常申明刪掉,並在實現裏面改拋爲運行時異常;2)添加一個方法isFoo,返回一個布爾值指示是否會有異常狀況出現;3)在foo調用前加一個if語句,判斷isFoo的返回值,若是爲真才調用foo,不然不調用;4)刪掉調用處的try-catch。
UI層處理異常的注意點:
UI層和其下邏輯層的區別是UI層的出錯信息是被用戶看,而其下層邏層出錯信息是被程序員看到,用戶可不但願看一個打印的異常棧,更不但願程序平白無故掛掉,用戶但願看到友好的提示信息。爲到達這一目的,咱們能夠設一個屏障,屏障能夠捕獲全部遺漏的異常,從而阻止程序直接掛掉,屏障固然恢復不了運行,但能夠記錄錯誤便於往後調試,還能夠輸出友好信息給用戶。Spring和Struts就有這樣的處理。
還有一點須要注意,用戶的傳入參數出現非法的機率很高,因此控制層接受到參數時必定要校驗,而不是原封不動的傳到其低層模塊。
附錄
在我查過的資料中,以《Effective Java》書中對異常處理設計的研究得最系統,本文不少思想來自於它,下面我把其中的幾條原則翻譯(非直譯)並貼上:
第57條:只對異常狀況使用異常。(說明:即不要用異常處理控制正常程序流)。
第58條:對可恢復異常使用編譯時異常,對編程錯誤使用運行時異常。
第59條:應避免沒必要要的編譯時異常:若是調用者即便合理的使用API也不能避免異常的發生,而且調用者能夠對捕獲的異常作出有意義的處理,才使用編譯時異常。
第60條:應偏好使用自帶異常
第61條:拋出的異常應適合本層抽象(就是上面說的轉譯)
第62條:把方法可能拋的全部異常寫入文檔,包括運行時異常
第63條:用異常類記錄的信息要包含失敗時的數據
第64條:力求失敗是原子化的(解釋:就是若是調用一個方法發生了異常,就應該使對象返回調用前的狀態)
第65條:不要忽略異常
參考資料:
Effective Java Exceptions, 譯文可參考這裏或這裏
原文轉載:http://blog.csdn.net/yanquan345/article/details/19633623