最近在閱讀《Java編程思想》的時候看到了書中對異常的描述,結合本身閱讀源碼經歷,談談本身對異常的理解。首先記住下面兩句話:html
除非你能解決(或必需要處理)這個異常,不然不要捕獲它,若是打算記錄錯誤消息,那麼別忘了把它再拋出去。java
異常既表明一種錯誤,又能夠表明一個消息。 程序員
這個問題其實不難理解,若是一切都按咱們設計好的進行,那麼通常(不通常的狀況是咱們設計的就是有缺陷的)是不會出現異常的,好比說一個除法操做:編程
public int div(int x,int y){ return x/y; }
固然咱們設計的是除數不能爲0,咱們也在方法名上添加了註釋,輸出不能爲0,若是用戶按照咱們的要求使用這個方法,固然不會有異常產生。但是不少時候,用戶不必定閱讀咱們的註釋,或者說,輸入的數據不是用戶主動指定的,而是程序計算的中間結果,這個時候就會致使除數爲0的狀況出現。api
如今異常狀況出現了,程序應該怎麼辦呢,直接掛掉確定是不行的,可是程序確實不能本身處理這種突發狀況,因此得想辦法把這種狀況告訴用戶,讓用戶本身來決定,也就是說程序須要把遇到的這種異常狀況包裝一下發送出去,由用戶來決定如何處理。數組
異常表示着一種信息。熟悉EOFException的程序員通常都會了解,這個異常,表示信息的成分大於表示出現了異常,不熟悉的參照我以前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107084.html。當這種情形下的異常(包括用戶自定義的大部分異常都屬於此類)出現時,是不須要解決的。安全
在繼續講解下面部分以前,仍是有必要了解下Java的異常分類的,經過Java API能夠看到以下繼承關係:服務器
簡單介紹一點:網絡
再把一開始說的那句話重複一遍,除非你能解決這個異常,不然不要捕獲它,若是打算記錄錯誤消息,那麼別忘了把它再拋出去。不過說真的,一個異常既然產生了,基本都是不能解決的,由於咱們的程序不能倒退到出現異常的代碼,更不能在相同輸入(不能改變輸入,否則結果還有什麼用),相同代碼(不能動態改變原有代碼)的狀況下來來讓它再也不出現異常,否則同一段代碼,在同一個輸入的狀況下有兩種不一樣的結果,誰還敢用呢?架構
除非咱們的程序須要依賴外部條件,而由外部條件致使的異常,咱們能夠改變外部條件使之知足程序要求,不過這種狀況基本均可以在程序執行前檢測出來。
舉兩個簡單的例子方便理解下,第一個是關於Socket的,具體Socket的知識能夠參考我以前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107785.html。
在Socket創建鏈接之後,咱們能夠經過Socket發送消息,高效的Socket利用方法是創建一個鏈接來持續使用,但是在這種狀況下,有一個須要注意的問題,那就是我在每次發送消息的時候,要不要檢測Socket是否還在鏈接中,個人在上面博客中介紹了,不須要。僞代碼以下:
//有一個鏈接中的socket Socket socket=... //要發送的數據 String data=""; try{ socket.write(data); }catch (Excetption e){ //打印日誌,並重連Socket socket=new Socket(host,port); socket.write(data); }
能夠看到,假如當前鏈接不可用(長時間不用被服務器主動斷開,或者網絡抖動致使的斷開),那麼咱們捕獲這個異常,而後從新創建一個鏈接來發送。這是最基本的解決方法,再高級一點的就是設置一個重複次數,當出現異常的時候重複發送指定的次數。
若是咱們仔細想一想,這個鏈接異常咱們沒有真正的解決它,而是經過又新建了一個鏈接來處理的,咱們解決的不是這個異常,而是發送數據出現了問題,咱們解決的是發送數據沒有成功這個問題。
一樣的,重複嘗試解決的偶發問題,這個偶發也是外部的條件致使的偶發,而不是程序自身問題。
通常的Web三層架構,action,server,Dao,若是出現異常後,再不知足上面解決條件的狀況下,若是都不捕獲異常,那麼用戶將會看到一個500頁面,附帶着堆棧信息,這種事不友好的表現方式,這種狀況下,咱們就須要在action層,用一個最大的try catch包住一個個方法,當出現異常的時候跳轉到錯誤頁面。
public String method(String param){ try { //邏輯處理 } catch (Exception e) { e.printStackTrace(); //跳轉到錯誤頁面 } }
實際上,咱們沒有解決異常,咱們只是解決了異常致使的問題,異常自己還在那,真正的解決方法就是程序員解決bug而後從新上線。
這種也算另類的解決,無可奈何不得不這麼作,實際上異常是被吞掉了,吞掉前留下了一點點信息。
首要條件仍是那句話,若是不能解決到出現異常的狀況,那就不要捕獲它,更不要吞掉他。
固然有的時候你會打算記錄異常的日誌,可是最開始也說過,異常也表明一個消息,就像IndexOutOfBoundsException、IOException自己的名字已經能夠代表異常的大部分信息,也就是說經過異常堆棧基本就能獲得關於異常部分的信息,可是有些異常堆棧沒有的是什麼呢,那就是發生異常條件時的外部信息。
固然在拋出異常的時候,虛擬機自己會盡量的打印出直接致使異常產生的輸入,但是當咱們還想獲取額外的環境信息的時候,咱們就須要捕獲異常,而後打印出來。
就像簡單的除0異常,以及字符串轉數字異常,自己異常堆棧就會提供基本的信息,可是若是咱們在一個用戶交互的環境下,假如咱們想要知道是哪一個用戶的輸入致使了異常的產生,這個時候系統產生的異常堆棧信息就不能知足咱們的要求了,而這個信息在當前類的一個字段中,這時候咱們就要主動捕獲而後打印出咱們想要的。
如今就這各類實例來講明異常怎麼處理。
假如說你寫了一個工具類,用於字符串和字節數組的UTF-8的轉換,假如以下:
package yiwangzhibujian.util; import java.io.UnsupportedEncodingException; public class Utils { public static String utf8(byte[] bytes) throws UnsupportedEncodingException{ return new String(bytes,"UTF-8"); } public static byte[] utf8(String str) throws UnsupportedEncodingException{ return str.getBytes("UTF-8"); } }
那麼用你工具類的人會頭疼死,明明不會有錯誤的,要麼拋出這個異常,要麼捕獲,實際上使用者根本不能解決這個異常。
因此有的人可能這麼作,他想既然這個異常必定不可能出現(本質上jvm必定能解析UTF-8的編碼,若是不能解析jvm也就不須要繼續運行了),那麼我就吞了它,什麼都不作:
package yiwangzhibujian.util; import java.io.UnsupportedEncodingException; public class Utils { public static String utf8(byte[] bytes){ try { return new String(bytes,"UTF-8"); } catch (UnsupportedEncodingException e) { } return null; } public static byte[] utf8(String str){ try { return str.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return null; } }
這麼作的人也有,不過這麼作的人也分爲兩種,一種是catch內什麼都不作,還有一種是catch內把異常信息打印出來,這兩種作法我比較傾向於後面那種,由於要考慮如下條件。
你認爲jvm必定能解析UTF-8,我不反對,但是你能保證你沒有拼錯UTF-8嗎,假如你寫成UFO-8呢?
public static String utf8(byte[] bytes){ try { return new String(bytes,"UFO-8"); } catch (UnsupportedEncodingException e) { } return null; }
那麼調用你的方法不只沒有錯誤提示,還致使返回了錯誤的結果,並致使後續一系列問題的產生,最致命的是 ,咱們根部不知道錯誤的根源在哪。
再舉一個對象克隆的例子。
package yiwangzhibujian.util; public class CloneTest { public static void main(String[] args) { Dog d1=new Dog("zhuzhuxia",26); Dog d2=d1.clone();//此處要麼捕獲要麼拋出 System.out.println(d1); System.out.println(d2); } } class Dog{ public String name; public int age; public Dog(String name, int age) { super(); this.name = name; this.age = age; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
能夠看到用戶調用你的對象的克隆方法是否是很痛苦,你既然提供給我克隆方法,就必定要能用,若是不能用,那麼拿回去重寫吧,我不會給你擦屁股的。因此咱們就會這麼作:
package yiwangzhibujian.util; public class CloneTest { public static void main(String[] args) { Dog d1=new Dog("zhuzhuxia",26); Dog d2=d1.clone();//此處要麼捕獲要麼拋出 System.out.println(d1); System.out.println(d2); } } class Dog{ public String name; public int age; public Dog(String name, int age) { super(); this.name = name; this.age = age; } @Override protected Dog clone() { try { return (Dog) super.clone(); } catch (Exception e) { e.printStackTrace();//不要省 } return null; } }
若是你運行上面的代碼的話,那麼就會拋出異常,由於咱們的類沒有實現Cloneable接口,緣由就是忘了寫,這在測試運行的首次就會發現並糾正。
java.lang.CloneNotSupportedException: yiwangzhibujian.util.Dog at java.lang.Object.clone(Native Method) at yiwangzhibujian.util.Dog.clone(CloneTest.java:22) at yiwangzhibujian.util.CloneTest.main(CloneTest.java:6) yiwangzhibujian.util.Dog@2a139a55 null
因此,我能夠假定這種狀況下不會出異常,可是咱們不能保證咱們沒有犯最基本的錯誤,因此錯誤堆棧仍是不能省的。
咱們來看一下jdk8中的HashMap關於克隆的處理:
@SuppressWarnings("unchecked") @Override public Object clone() { HashMap<K,V> result; try { result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } result.reinitialize(); result.putMapEntries(this, false); return result; }
是否是不會拋必須捕獲的異常,它還作了更高級的事,那就是我拋一個ERROR,通常咱們的程序都是捕獲Exception,不會捕捉這個異常,這個異常會一直向上傳播。
那麼打印異常堆棧和拋出ERROR哪一種更好呢,個人建議是拋出ERROR:
咱們再拿上面的字符串,字節數組例子來講明,咱們對它進行了升級,下面是不完整代碼:
public static String byteToStr(byte[] bytes, String charsetName) { return new String(bytes, charsetName); }
應該怎麼作,拋異常?捕獲異常打印日誌?兩種作法都很差:
這種狀況下應該怎麼作呢,比較推薦的作法就是包裝成運行時異常拋出:
public static String byteToStr(byte[] bytes, String charsetName) { try { return new String(bytes, charsetName); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } }
這麼作就解決了上面的兩個問題。
你的代碼必定會出異常,那你仍是拿回去重寫吧。除非你不想讓別人調用你的方法,好比說不可變容器的操做類方法都將拋出異常。
在你不主動吞併異常的狀況下,異常是不會丟失的,可是有一種特殊情形須要注意,那就是finally中有return的狀況(代碼參照Java編程思想):
public static void ExceptionSilencer(){ try { throw new RuntimeException(); } finally { return; } }
這種狀況下,異常就會丟了,完徹底全消失不見了,因此要避免這麼使用,避免finally中使用return。
這個異常是歸於ERROR級別的,Java api也對此有相應介紹:
The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it.
就是說ThreadDeath自己是一個普通的異常,這個異常出現應該致使線程死亡,可是不把它歸於Exception的緣由就是,jdk的開發者也料到Java程序員最喜歡try catch異常而後吞掉了,這樣將會致使本該死亡的線程繼續運行下去,這是不該該的。並且當這個異常出現時會終結線程,可是不會打印出任何異常堆棧信息。
這個異常比較少見,Thread的stop方法,會產生這個異常。
若是你的線程常常莫名其妙的消失,而沒有任何相關日誌,你能夠嘗試捕獲這個異常,可是記住,打印完相關日誌再把它從新拋出去。
下面摘自Java編程思想的異常使用指南,特別好,必定要深刻理解一下:
下面依次說下個人想法。
以上就是我對異常的理解,但願能夠幫助到有須要的人,若是你能認真看完我相信你會有收穫的,若是錯誤請指出,禁止轉載。