看完這篇Exception 和 Error,和面試官扯皮就沒問題了

在 Java 中的基本理念是 結構不佳的程式碼不能運行,發現錯誤的理想時期是在編譯期間,因為你不用運行程式,只是憑藉著對 Java 基本理念的理解就能發現問題。但是編譯期並不能找出所有的問題,有一些 NullPointerException 和 ClassNotFoundException 在編譯期找不到,這些異常是 RuntimeException 運行時異常,這些異常往往在運行時才能被發現。

我們寫 Java 程式經常會出現兩種問題,一種是 java.lang.Exception ,一種是 java.lang.Error,都用來表示出現了異常情況,下面就針對這兩種概念進行理解。

認識 Exception

Exception 位於 java.lang 包下,它是一種頂級介面,繼承於 Throwable 類,Exception 類及其子類都是 Throwable 的組成條件,是程式出現的合理情況。

在認識 Exception 之前,有必要先了解一下什麼是 Throwable

什麼是 Throwable

Throwable 類是 Java 語言中所有錯誤(errors)異常(exceptions)的父類。只有繼承於 Throwable 的類或者其子類才能夠被拋出,還有一種方式是帶有 Java 中的 @throw 註解的類也可以拋出。

Java規範中,對非受查異常和受查異常的定義是這樣的:

The unchecked exception classes are the run-time exception classes and the error classes.

The checked exception classes are all exception classes other than the unchecked exception classes. That is, the checked exception classes are Throwable and all its subclasses other than RuntimeException and its subclasses and Errorand its subclasses.

也就是說,除了 RuntimeException 和其子類,以及error和其子類,其它的所有異常都是 checkedException

那麼,按照這種邏輯關係,我們可以對 Throwable 及其子類進行歸類分析

可以看到,Throwable 位於異常和錯誤的最頂層,我們查看 Throwable 類中發現它的方法和屬性有很多,我們只討論其中幾個比較常用的

// 返回拋出異常的詳細資訊  public string getMessage();  public string getLocalizedMessage();    //返回異常發生時的簡要描述  public public String toString();    // 列印異常資訊到標準輸出流上  public void printStackTrace();  public void printStackTrace(PrintStream s);  public void printStackTrace(PrintWriter s)    // 記錄棧幀的的當前狀態  public synchronized Throwable fillInStackTrace();  

此外,因為 Throwable 的父類也是 Object,所以常用的方法還有繼承其父類的getClass()getName() 方法。

常見的 Exception

下面我們回到 Exception 的探討上來,現在你知道了 Exception 的父類是 Throwable,並且 Exception 有兩種異常,一種是 RuntimeException ;一種是 CheckedException,這兩種異常都應該去捕獲

下面列出了一些 Java 中常見的異常及其分類,這塊面試官也可能讓你舉出幾個常見的異常情況並將其分類

RuntimeException

序號 異常名稱 異常描述
1 ArrayIndexOutOfBoundsException 數組越界異常
2 NullPointerException 空指針異常
3 IllegalArgumentException 非法參數異常
4 NegativeArraySizeException 數組長度為負異常
5 IllegalStateException 非法狀態異常
6 ClassCastException 類型轉換異常

UncheckedException

序號 異常名稱 異常描述
1 NoSuchFieldException 表示該類沒有指定名稱拋出來的異常
2 NoSuchMethodException 表示該類沒有指定方法拋出來的異常
3 IllegalAccessException 不允許訪問某個類的異常
4 ClassNotFoundException 類沒有找到拋出異常

與 Exception 有關的 Java 關鍵字

那麼 Java 中是如何處理這些異常的呢?在 Java 中有這幾個關鍵字 throws、throw、try、finally、catch 下面我們分別來探討一下

throws 和 throw

在 Java 中,異常也就是一個對象,它能夠被程式設計師自定義拋出或者應用程式拋出,必須藉助於 throwsthrow 語句來定義拋出異常。

throws 和 throw 通常是成對出現的,例如

static void cacheException() throws Exception{      throw new Exception();    }  

throw 語句用在方法體內,表示拋出異常,由方法體內的語句處理。
throws 語句用在方法聲明後面,表示再拋出異常,由該方法的調用者來處理。

throws 主要是聲明這個方法會拋出這種類型的異常,使它的調用者知道要捕獲這個異常。
throw 是具體向外拋異常的動作,所以它是拋出一個異常實例。

try 、finally 、catch

這三個關鍵字主要有下面幾種組合方式 try…catch 、try…finally、try…catch…finally

try…catch 表示對某一段程式碼可能拋出異常進行的捕獲,如下

static void cacheException() throws Exception{      try {      System.out.println("1");    }catch (Exception e){      e.printStackTrace();    }    }  

try…finally 表示對一段程式碼不管執行情況如何,都會走 finally 中的程式碼

static void cacheException() throws Exception{    for (int i = 0; i < 5; i++) {      System.out.println("enter: i=" + i);      try {        System.out.println("execute: i=" + i);        continue;      } finally {        System.out.println("leave: i=" + i);      }    }  }  

try…catch…finally 也是一樣的,表示對異常捕獲後,再走 finally 中的程式碼邏輯。

JDK1.7 使用 try…with…resources 優雅關閉資源

Java 類庫中有許多資源需要通過 close 方法進行關閉。比如 InputStream、OutputStream,資料庫連接對象 Connection,MyBatis 中的 SqlSession 會話等。作為開發人員經常會忽略掉資源的關閉方法,導致記憶體泄漏。

根據經驗,try-finally語句是確保資源會被關閉的最佳方法,就算異常或者返回也一樣。try-catch-finally 一般是這樣來用的

static String firstLineOfFile(String path) throws IOException {    BufferedReader br = new BufferedReader(new FileReader(path));    try {      return br.readLine();    }finally {      br.close();    }  }  

這樣看起來程式碼還是比較整潔,但是當我們添加第二個需要關閉的資源的時候,就像下面這樣

static void copy(String src,String dst) throws Exception{          InputStream is = new FileInputStream(src);    try {        OutputStream os = new FileOutputStream(dst);      try {        byte[] buf = new byte[100];        int n;        while ((n = is.read()) >= 0){          os.write(buf,n,0);        }      }finally {        os.close();      }    }finally {      is.close();    }  }  

這樣感覺這個方法已經變得臃腫起來了。

而且這種寫法也存在諸多問題,即使 try – finally 能夠正確關閉資源,但是它不能阻止異常的拋出,因為 try 和 finally 塊中都可能有異常的發生。

比如說你正在讀取的時候硬碟損壞,這個時候你就無法讀取文件和關閉資源了,此時會拋出兩個異常。但是在這種情況下,第二個異常會抹掉第一個異常。在異常堆棧中也無法找到第一個異常的記錄,怎麼辦,難道像這樣來捕捉異常么?

static void tryThrowException(String path) throws Exception {      BufferedReader br = new BufferedReader(new FileReader(path));    try {      String s = br.readLine();      System.out.println("s = " + s);      }catch (Exception e){      e.printStackTrace();    }finally {      try {        br.close();      }catch (Exception e){        e.printStackTrace();      }finally {        br.close();      }    }  }  

這種寫法,雖然能解決異常拋出的問題,但是各種 try-cath-finally 的嵌套會讓程式碼變得非常臃腫。

Java7 中引入了try-with-resources 語句時,所有這些問題都能得到解決。要使用 try-with-resources 語句,首先要實現 AutoCloseable 介面,此介面包含了單個返回的 close 方法。Java 類庫與三方類庫中的許多類和介面,現在都實現或者擴展了 AutoCloseable 介面。如果編寫了一個類,它代表的是必須關閉的資源,那麼這個類應該實現 AutoCloseable 介面。

java 引入了 try-with-resources 聲明,將 try-catch-finally 簡化為 try-catch,這其實是一種語法糖,在編譯時會進行轉化為 try-catch-finally 語句。

下面是使用 try-with-resources 的第一個範例

/**       * 使用try-with-resources 改寫示例一       * @param path       * @return       * @throws IOException       */  static String firstLineOfFileAutoClose(String path) throws IOException {      try(BufferedReader br = new BufferedReader(new FileReader(path))){      return br.readLine();    }  }  

使用 try-with-resources 改寫程式的第二個示例

static void copyAutoClose(String src,String dst) throws IOException{      try(InputStream in = new FileInputStream(src);        OutputStream os = new FileOutputStream(dst)){      byte[] buf = new byte[1000];      int n;      while ((n = in.read(buf)) >= 0){        os.write(buf,0,n);      }    }  }  

使用 try-with-resources 不僅使程式碼變得通俗易懂,也更容易診斷。以firstLineOfFileAutoClose方法為例,如果調用 readLine() close() 方法都拋出異常,後一個異常就會被禁止,以保留第一個異常。

異常處理的原則

我們在日常處理異常的程式碼中,應該遵循三個原則

  • 不要捕獲類似 Exception 之類的異常,而應該捕獲類似特定的異常,比如 InterruptedException,方便排查問題,而且也能夠讓其他人接手你的程式碼時,會減少罵你的次數。
  • 不要生吞異常。這是?異常處理中要特別注重?的事情,因為很可能會??非常難以??正常??結束情況。如果我們不把?異常拋?出來,或者也沒有輸?出到 ???Logger? 日誌中,程式可能會在後面以不可控的方式結束。
  • 不要在函數式編程中使用 checkedException

什麼是 Error

Error 是程式無法處理的錯誤,表示運行應用程式中較嚴重問題。大多數錯誤與程式碼編寫者執行的操作無關,而表示程式碼運行時 JVM(Java 虛擬機)出現的問題。這些錯誤是不可檢查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程式運行時不允許出現的狀況,比如 OutOfMemoryErrorStackOverflowError異常的出現會有幾種情況,這裡需要先介紹一下 Java 記憶體模型 JDK1.7。

其中包括兩部分,由所有執行緒共享的數據區和執行緒隔離的數據區組成,在上面的 Java 記憶體模型中,只有程式計數器是不會發生 OutOfMemoryError 情況的區域,程式計數器控制著電腦指令的分支、循環、跳轉、異常處理和執行緒恢復,並且程式計數器是每個執行緒私有的。

什麼是執行緒私有:表示的就是各條執行緒之間互不影響,獨立存儲的記憶體區域。

如果應用程式執行的是 Java 方法,那麼這個計數器記錄的就是虛擬機位元組碼指令的地址;如果正在執行的是 Native 方法,這個計數器值則為空(Undefined)

除了程式計數器外,其他區域:方法區(Method Area)虛擬機棧(VM Stack)本地方法棧(Native Method Stack)堆(Heap) 都是可能發生 OutOfMemoryError 的區域。

  • 虛擬機棧:如果執行緒請求的棧深度大於虛擬機棧所允許的深度,將會出現 StackOverflowError 異常;如果虛擬機動態擴展無法申請到足夠的記憶體,將出現 OutOfMemoryError

  • 本地方法棧和虛擬機棧一樣

  • 堆:Java 堆可以處於物理上不連續,邏輯上連續,就像我們的磁碟空間一樣,如果堆中沒有記憶體完成實例分配,並且堆無法擴展時,將會拋出 OutOfMemoryError。

  • 方法區:方法區無法滿足記憶體分配需求時,將拋出 OutOfMemoryError 異常。

一道經典的面試題

一道非常經典的面試題,NoClassDefFoundError 和 ClassNotFoundException 有什麼區別

在類的載入過程中, JVM 或者 ClassLoader 無法找到對應的類時,都可能會引起這兩種異常/錯誤,由於不同的 ClassLoader 會從不同的地方載入類,有時是錯誤的 CLASSPATH 類路徑導致的這類錯誤,有時是某個庫的 jar 包缺失引發這類錯誤。NoClassDefFoundError 表示這個類在編譯時期存在,但是在運行時卻找不到此類,有時靜態初始化塊也會導致 NoClassDefFoundError 錯誤。

ClassLoader 是類路徑裝載器,在Java 中,類路徑裝載器一共有三種兩類

一種是虛擬機自帶的 ClassLoader,分為三種

  • 啟動類載入器(Bootstrap) ,負責載入 $JAVAHOME/jre/lib/rt.jar
  • 擴展類載入器(Extension),負責載入 $JAVAHOME/jre/lib/ext/*.jar
  • 應用程式類載入器(AppClassLoader),載入當前應用的 classpath 的所有類

第二種是用戶自定義類載入器

  • Java.lang.ClassLoader 的子類,用戶可以訂製類的載入方式。

另一方面,ClassNotFoundException 與編譯時期無關,當你嘗試在運行時使用反射載入類時,ClassNotFoundException 就會出現。

簡而言之,ClassNotFoundException 和 NoClassDefFoundError 都是由 CLASSPATH 中缺少類引起的,通常是由於缺少 JAR 文件而引起的,但是如果 JVM 認為應用運行時找不到相應的引用,就會拋出 NoClassDefFoundError 錯誤;當你在程式碼中顯示的載入類比如 Class.forName() 調用時卻沒有找到相應的類,就會拋出 java.lang.ClassNotFoundException

  • NoClassDefFoundError 是 JVM 引起的錯誤,是 unchecked,未經檢查的。因此不會使用 try-catch 或者 finally 語句塊;另外,ClassNotFoundException 是受檢異常,因此需要 try-catch 語句塊或者 try-finally 語句塊包圍,否則會導致編譯錯誤。
  • 調用 Class.forName()、ClassLoader.findClass() 和 ClassLoader.loadClass() 等方法時可能會引起 java.lang.ClassNotFoundException,如圖所示

  • NoClassDefFoundError 是鏈接錯誤,發生在鏈接階段,當解析引用找不到對應的類,就會觸發;而 ClassNotFoundException 是發生在運行時的異常。

文章參考:

https://www.java67.com/2012/12/noclassdeffounderror-vs-classnotfoundexception-java.html

《極客時間-Java核心技術 36 講》

《深入理解 Java 虛擬機》第二版

《Effective Java 第三版》

https://www.cnblogs.com/xiohao/p/3547443.html

https://blog.csdn.net/qq_29229567/article/details/80773970

https://blog.csdn.net/riemann_/article/details/87522352

《Java編程思想》

https://www.cnblogs.com/xz816111/p/8466048.html

https://docs.oracle.com/javase/specs/jls/se9/html/jls-11.html#jls-11.1.1

jdk 1.8 源碼注釋