Java異常體系簡析


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

  除非你能解決(或必須要處理)這個異常,否則不要捕獲它,如果打算記錄錯誤消息,那么別忘了把它再拋出去。

  異常既代表一種錯誤,又可以代表一個消息。 

一、為什么會有異常

  這個問題其實不難理解,如果一切都按我們設計好的進行,那么一般(不一般的情況是我們設計的就是有缺陷的)是不會出現異常的,比如說一個除法操作:

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

  當然我們設計的是除數不能為0,我們也在方法名上添加了注釋,輸出不能為0,如果用戶按照我們的要求使用這個方法,當然不會有異常產生。可是很多時候,用戶不一定閱讀我們的注釋,或者說,輸入的數據不是用戶主動指定的,而是程序計算的中間結果,這個時候就會導致除數為0的情況出現。

  現在異常情況出現了,程序應該怎么辦呢,直接掛掉肯定是不行的,但是程序確實不能自己處理這種突發情況,所以得想辦法把這種情況告訴用戶,讓用戶自己來決定,也就是說程序需要把遇到的這種異常情況包裝一下發送出去,由用戶來決定如何處理。

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

二、Java異常的分類

  在繼續講解下面部分之前,還是有必要了解下Java的異常分類的,通過Java API可以看到如下繼承關系:

  簡單介紹一點:

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

三、異常的處理的理解

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

  除非我們的程序需要依賴外部條件,而由外部條件導致的異常,我們可以改變外部條件使之滿足程序要求,不過這種情況基本都可以在程序執行前檢測出來。

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

  如果你的線程經常莫名其妙的消失,而沒有任何相關日志,你可以嘗試捕獲這個異常,但是記住,打印完相關日志再把它重新拋出去。

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

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

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

  下面依次說下我的想法。

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

 

  以上就是我對異常的理解,希望可以幫助到有需要的人,如果你能認真看完我相信你會有收獲的,如果錯誤請指出,禁止轉載。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM