Java異常體系簡析

  最近在閱讀《Java編程思想》的時候看到了書中對異常的描述,結合本身閱讀源碼經歷,談談本身對異常的理解。首先記住下面兩句話:html

  除非你能解決(或必需要處理)這個異常,不然不要捕獲它,若是打算記錄錯誤消息,那麼別忘了把它再拋出去。java

  異常既表明一種錯誤,又能夠表明一個消息。 程序員

1、爲何會有異常

  這個問題其實不難理解,若是一切都按咱們設計好的進行,那麼通常(不通常的狀況是咱們設計的就是有缺陷的)是不會出現異常的,好比說一個除法操做:編程

public int div(int x,int y){
  return x/y;
}

  固然咱們設計的是除數不能爲0,咱們也在方法名上添加了註釋,輸出不能爲0,若是用戶按照咱們的要求使用這個方法,固然不會有異常產生。但是不少時候,用戶不必定閱讀咱們的註釋,或者說,輸入的數據不是用戶主動指定的,而是程序計算的中間結果,這個時候就會致使除數爲0的狀況出現。api

  如今異常狀況出現了,程序應該怎麼辦呢,直接掛掉確定是不行的,可是程序確實不能本身處理這種突發狀況,因此得想辦法把這種狀況告訴用戶,讓用戶本身來決定,也就是說程序須要把遇到的這種異常狀況包裝一下發送出去,由用戶來決定如何處理。數組

  異常表示着一種信息。熟悉EOFException的程序員通常都會了解,這個異常,表示信息的成分大於表示出現了異常,不熟悉的參照我以前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107084.html。當這種情形下的異常(包括用戶自定義的大部分異常都屬於此類)出現時,是不須要解決的。安全

2、Java異常的分類

  在繼續講解下面部分以前,仍是有必要了解下Java的異常分類的,經過Java API能夠看到以下繼承關係:服務器

  簡單介紹一點:網絡

  • Throwable是全部異常的父類
  • Error表示很嚴重的問題發生了,能夠捕獲可是不要捕獲,由於捕獲了也解決不了,這個不是由程序產出的,底層出現問題就讓他它掛了吧。

3、異常的處理的理解

  再把一開始說的那句話重複一遍,除非你能解決這個異常,不然不要捕獲它,若是打算記錄錯誤消息,那麼別忘了把它再拋出去。不過說真的,一個異常既然產生了,基本都是不能解決的,由於咱們的程序不能倒退到出現異常的代碼,更不能在相同輸入(不能改變輸入,否則結果還有什麼用),相同代碼(不能動態改變原有代碼)的狀況下來來讓它再也不出現異常,否則同一段代碼,在同一個輸入的狀況下有兩種不一樣的結果,誰還敢用呢?架構

  除非咱們的程序須要依賴外部條件,而由外部條件致使的異常,咱們能夠改變外部條件使之知足程序要求,不過這種狀況基本均可以在程序執行前檢測出來。

3.1 怎麼纔算解決異常

  舉兩個簡單的例子方便理解下,第一個是關於Socket的,具體Socket的知識能夠參考我以前的博客:http://www.cnblogs.com/yiwangzhibujian/p/7107785.html

3.1.1 重複嘗試解決偶發問題

  在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);
}

  能夠看到,假如當前鏈接不可用(長時間不用被服務器主動斷開,或者網絡抖動致使的斷開),那麼咱們捕獲這個異常,而後從新創建一個鏈接來發送。這是最基本的解決方法,再高級一點的就是設置一個重複次數,當出現異常的時候重複發送指定的次數。

  若是咱們仔細想一想,這個鏈接異常咱們沒有真正的解決它,而是經過又新建了一個鏈接來處理的,咱們解決的不是這個異常,而是發送數據出現了問題,咱們解決的是發送數據沒有成功這個問題。

  一樣的,重複嘗試解決的偶發問題,這個偶發也是外部的條件致使的偶發,而不是程序自身問題。

3.1.2 不想看到錯誤堆棧

  通常的Web三層架構,action,server,Dao,若是出現異常後,再不知足上面解決條件的狀況下,若是都不捕獲異常,那麼用戶將會看到一個500頁面,附帶着堆棧信息,這種事不友好的表現方式,這種狀況下,咱們就須要在action層,用一個最大的try catch包住一個個方法,當出現異常的時候跳轉到錯誤頁面。

public String method(String param){
  try {
    //邏輯處理
  } catch (Exception e) {
    e.printStackTrace();
    //跳轉到錯誤頁面
  }
}

  實際上,咱們沒有解決異常,咱們只是解決了異常致使的問題,異常自己還在那,真正的解決方法就是程序員解決bug而後從新上線。

  這種也算另類的解決,無可奈何不得不這麼作,實際上異常是被吞掉了,吞掉前留下了一點點信息。

3.2 咱們應該怎麼作

  首要條件仍是那句話,若是不能解決到出現異常的狀況,那就不要捕獲它,更不要吞掉他。

  固然有的時候你會打算記錄異常的日誌,可是最開始也說過,異常也表明一個消息,就像IndexOutOfBoundsException、IOException自己的名字已經能夠代表異常的大部分信息,也就是說經過異常堆棧基本就能獲得關於異常部分的信息,可是有些異常堆棧沒有的是什麼呢,那就是發生異常條件時的外部信息。

  固然在拋出異常的時候,虛擬機自己會盡量的打印出直接致使異常產生的輸入,但是當咱們還想獲取額外的環境信息的時候,咱們就須要捕獲異常,而後打印出來。

  就像簡單的除0異常,以及字符串轉數字異常,自己異常堆棧就會提供基本的信息,可是若是咱們在一個用戶交互的環境下,假如咱們想要知道是哪一個用戶的輸入致使了異常的產生,這個時候系統產生的異常堆棧信息就不能知足咱們的要求了,而這個信息在當前類的一個字段中,這時候咱們就要主動捕獲而後打印出咱們想要的。

4、異常的處理

  如今就這各類實例來講明異常怎麼處理。

4.1 對認爲必定不會出現的異常

  假如說你寫了一個工具類,用於字符串和字節數組的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:

  • 能出現這種狀況也就表明jvm出現了問題,或許其餘基本功能也出現了問題,應該當即停掉重啓並解決問題,否則數據都有可能出現錯誤。
  • 若是打印堆棧信息,那麼下次調用仍是會出錯,不如直接拋ERROR,若是上層沒有作具體的應對jvm應該會中止。

4.2 對假定不該該出現的異常

  咱們再拿上面的字符串,字節數組例子來講明,咱們對它進行了升級,下面是不完整代碼:

public static String byteToStr(byte[] bytes, String charsetName) {
  return new String(bytes, charsetName);
}

  應該怎麼作,拋異常?捕獲異常打印日誌?兩種作法都很差:

  • 若是拋異常:那麼使用你工具類的人依然很頭疼,他必須在每次調用你方法的時候作處理,要麼拋要麼捕獲,而他在想我明明傳入一個UTF-8,非得給我拋異常,難用死了。
  • 若是捕獲打印日誌:這個更不可取,若是用戶輸錯了編碼類型,那麼你將不能給出任何信息給調用者(打印日誌只能過後找錯),用戶認爲寫的沒錯而你也給出了返回值,這也會致使一系列錯誤的產生。

  這種狀況下應該怎麼作呢,比較推薦的作法就是包裝成運行時異常拋出:

public static String byteToStr(byte[] bytes, String charsetName) {
  try {
    return new String(bytes, charsetName);
  } catch (UnsupportedEncodingException e) {
    throw new RuntimeException(e);
  }
}

  這麼作就解決了上面的兩個問題。

4.3 對假定必定出異常的狀況

  你的代碼必定會出異常,那你仍是拿回去重寫吧。除非你不想讓別人調用你的方法,好比說不可變容器的操做類方法都將拋出異常。

5、異常的一些特殊狀況

5.1 防止異常丟失

  在你不主動吞併異常的狀況下,異常是不會丟失的,可是有一種特殊情形須要注意,那就是finally中有return的狀況(代碼參照Java編程思想):

public static void ExceptionSilencer(){
  try {
    throw new RuntimeException();
  } finally {
    return;
  }
}

  這種狀況下,異常就會丟了,完徹底全消失不見了,因此要避免這麼使用,避免finally中使用return。

5.2 線程中ThreadDeath異常

  這個異常是歸於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方法,會產生這個異常。

  若是你的線程常常莫名其妙的消失,而沒有任何相關日誌,你能夠嘗試捕獲這個異常,可是記住,打印完相關日誌再把它從新拋出去。

6、Java編程思想中關於總結的解讀

  下面摘自Java編程思想的異常使用指南,特別好,必定要深刻理解一下:

  1. 在恰當的級別處理問題。(在知道該如何處理的狀況下了捕獲異常。)
  2. 解決問題而且從新調用產生異常的方法。
  3. 進行少量修補,而後繞過異常發生的地方繼續執行。
  4. 用別的數據進行計算,以代替方法預計會返回的值。
  5. 把當前運行環境下能作的事儘可能作完,而後把相同的異常重拋到更高層
  6. 把當前運行環境下能作的事儘可能作完,而後把不一樣的異常拋到更高層
  7. 終止程序
  8. 進行簡化(若是你的異常模式使問題變得太複雜,那麼用起來會很是痛苦)。
  9. 讓類庫和程序更安全。

  下面依次說下個人想法。

  • 第1條:上面章節已經介紹了,此處再也不說明
  • 第2條:上面也介紹過,就是外部條件致使的,能夠重複執行可能正常的代碼
  • 第3條:這種狀況實質上也是吞併異常,好比說網絡爬蟲,當遇到死連接的時候,可能會拋出鏈接異常等,此時拋棄這個鏈接也是能夠的,這個偏差能夠接收
  • 第4條:有的程序員會這麼設計,當出現用戶輸出錯誤數據致使異常的時候,就用一個默認的值來代替,我不喜歡這麼作,我會直接拋異常讓使用者去更改,若是非要這麼作必定要打印好相關日誌
  • 第5條:這種狀況看需求,若是要求要麼所有成功,要麼都不作,那麼就不適合這種狀況
  • 第6條:同上,可是我不太理解這個
  • 第7條:這個就不要了吧,出現一個異常程序就掛了,那也太脆了,不過當程序在正常啓動過程當中,若是出現異常就直接掛掉仍是合理的,讓用戶修改外部條件保障啓動沒有問題,好比說用戶指定的配置文件不存在(或許他寫錯了路徑),那麼不要使用默認配置,程序直接掛掉就能夠了,否則會給用戶一種按照他的配置成功啓動的錯覺。
  • 第8條:這個和上面說到的用運行時異常來包裹捕獲異常一個性質。
  • 第9條:這個是終極目標,考慮全部的狀況,把異常消滅在萌芽中,過於理想了。通常越安全越健壯的程序考慮的異常條件就越多。通常都會在使用前作各類判斷,條件是否知足,輸入是否正確等。

 

  以上就是我對異常的理解,但願能夠幫助到有須要的人,若是你能認真看完我相信你會有收穫的,若是錯誤請指出,禁止轉載。

相關文章
相關標籤/搜索