1. 程式人生 > >Java 異常相關面試題

Java 異常相關面試題

問:java 異常有哪幾種,特點是什麼?

答:異常是發生在程式執行過程中阻礙程式正常執行的錯誤操作,只要在 Java 語句執行中產生異常則一個異常物件就會被建立。Throwable 是所有異常的父類,它有兩個直接子類 Error 和 Exception,其中 Exception 又被繼續劃分為被檢查的異常(checked exception)和執行時的異常(runtime exception,即不受檢查的異常);Error 表示系統錯誤,通常不能預期和恢復(譬如 JVM 崩潰、記憶體不足等);被檢查的異常(Checked exception)在程式中能預期且要嘗試修復(如我們必須捕獲 FileNotFoundException 異常併為使用者提供有用資訊和合適日誌來進行除錯,Exception 是所有被檢查的異常的父類);執行時異常(Runtime Exception)又稱為不受檢查異常,譬如我們檢索陣列元素之前必須確認陣列的長度,否則就可能會丟擲 ArrayIndexOutOfBoundException 執行時異常,RuntimeException 是所有執行時異常的父類。

 

問:java 中 throw 與 throws 的區別是什麼?

答:throw 使用的位置在方法中,後面跟的異常物件例項,表示丟擲異常,由方法體內語句處理,如果方法中有 throw 丟擲 RuntimeException 及其子類則宣告上可以沒有 throws,如果方法中有 throw 丟擲 Exception 及其子類則宣告上必須有 throws。throws 使用的位置在方法引數小括號後面,後面跟的是一個或者多個異常類名且用逗號隔開,表示丟擲異常並交給呼叫者去處理,如果後面根據的是 RuntimeException 及其子類則該方法可以不用處理,如果後面根據的是 Exception 及其子類則必須要編寫程式碼進行處理或者呼叫的時候丟擲。

 

問:java 中被檢查的異常和不受檢查的異常有什麼區別?

答:被檢查的異常應該用 try-catch 塊程式碼處理或用 throws 關鍵字丟擲,不受檢查的異常在程式中不要求被處理或用 throws 丟擲;Exception 是所有被檢查異常的基類,而 RuntimeException(是 Exception 的子類) 是所有不受檢查異常的基類;被檢查的異常適用於那些不是因程式引起的錯誤情況(如 FileNotFoundException),而不被檢查的異常通常都是由於糟糕的程式設計引起(如 NullPointerException)。

 

問:java 中 Error 和 Exception 有什麼區別?

答:Error 表示系統級的錯誤,是 java 執行環境內部錯誤或者硬體問題,不能指望程式來處理這樣的問題,除了退出執行外別無選擇,它是 java 虛擬機器丟擲的。Exception 表示程式需要捕捉、需要處理的異常,是由與程式設計的不完善而出現的問題,程式可以處理的問題。

 

問:java 中什麼是異常鏈?

答:異常鏈是指在進行一個異常處理時丟擲了另外一個異常,由此產生了一個異常鏈條,大多用於將受檢查異常(checked exception)封裝成為非受檢查異常(unchecked exception)或者 RuntimeException。特別注意如果你因為一個異常而決定丟擲另一個新的異常時一定要包含原有的異常,這樣處理程式才可以通過 getCause() 和 initCause() 方法來訪問異常最終的根源。

 

問:java 中如何編寫自定義異常?

答:可以通過繼承 Exception 類或其任何子類來實現自己的自定義異常類,自定義異常類可以有自己的變數和方法來傳遞錯誤程式碼或其它異常相關資訊來處理異常。下面是一個自定義異常的常見模板:

public class DemoException extends IOException {

   private static final long serialVersionUID = 123456789L;

   private String errorCode="DemoException";


   public DemoException(String msg, String errorCode){
       super(msg);
       this.errorCode = errorCode;
   }


   public String getErrorCode(){
       return this.errorCode;
   }
}

 

問:請簡單描述下面方法的執行流程和最終返回值是多少?

public static int test1(){
   int ret = 0;
   try{
       return ret;
   }finally{
       ret = 2;
   }
}
public static int test2(){
   int ret = 0;
   try{
       int a = 5/0;
       return ret;
   }finally{
       return 2;
   }
}

public static void test3(){
   try{
       int a = 5/0;
   }finally{
       throw new RuntimeException("hello");
   }
}

答:本題旨在考察 try-catch-finally 塊的用法踩坑經驗,具體解析如下。

test1 方法執行返回 0,因為執行到 try 的 return ret; 語句前會先將返回值 ret 儲存在一個臨時變數中,然後才執行 finally 語句,最後 try 再返回那個臨時變數,finally 中對 ret 的修改不會被返回。

test2 方法執行返回 2,因為 5/0 會觸發 ArithmeticException 異常,但是 finally 中有 return 語句,finally 中 return 不僅會覆蓋 try 和 catch 內的返回值且還會掩蓋 try 和 catch 內的異常,就像異常沒有發生一樣(特別注意,當 finally 中沒有 return 時該方法執行會丟擲 ArithmeticException 異常),所以這個方法就會返回 2,而且不再向上傳遞異常了。

test3 方法執行丟擲 hello 異常,因為如果 finally 中丟擲了異常,則原異常就會被掩蓋。

因此為避免程式碼邏輯混淆,我們應該避免在 finally 中使用 return 語句或者丟擲異常,如果呼叫的其他程式碼可能丟擲異常,則應該捕獲異常並進行處理。

 

問:如果執行 finally 程式碼塊之前方法返回了結果或者 JVM 退出了,這時 finally 塊中的程式碼還會執行嗎?

答:只有在 try 裡面通過 System.exit(0) 來退出 JVM 的情況下 finally 塊中的程式碼才不會執行,其他 return 等情況都會呼叫,所以在不終止 JVM 的情況下 finally 中的程式碼一定會執行。

 

問:分別說說下面程式碼片段都有什麼問題?

public static void func() throws RuntimeException, NullPointerException {
   throw new RuntimeException("func exception");
}


public static void main(String args[]) {
   try {
       func();
   } catch (Exception ex) {
       ex.printStackTrace();
   } catch (RuntimeException re) {
       re.printStackTrace();
   }
}

上面程式碼段對於 func 方法後面 throws 列出的異常型別是不分先後順序的,所以 func 方法是沒問題的;對於 main 方法中在捕獲 RuntimeException 型別變數 re 的地方會編譯錯誤,因為 Exception 是 RuntimeException 的超類,func 方法執行的異常都會被第一個 catch 塊捕獲,所以會報編譯時錯誤。

public class Base {
   public void func() throws IOException {
       throw new IOException("Base IOException");
   }
}


public class Sub extends Base {
   public void func() throws Exception {
       throw new Exception("Sub Exception");
   }
}

如上程式碼片段在編譯時子類 func 方法會出現編譯異常,因為在 java 中重寫方法丟擲的異常不能是原方法丟擲異常的父類,這裡 func 方法在父類中丟擲了 IOException,所有在子類中的 func 方法只能丟擲 IOExcepition 或是其子類,但不能是其父類。

public static void func() {}


public static void main(String args[]) {
   try{
       func();
   }catch(IOException e) {
       e.printStackTrace();
   }
}

上面程式碼段編譯時在 IOException 時會出現編譯錯誤,因為 IOException 是受檢查異常,而 func 方法並沒有丟擲 IOException,所以編譯報錯,但是如果將 IOException 改為 Exception(或者 NullPointerException 等)則編譯報錯將消失,因為 Exception 可以用來捕捉所有執行時異常,這樣就不需要宣告丟擲語句。

答:通過上面幾個程式碼片段可以看出我們在書寫多 catch 塊時要保證異常型別的優先順序書寫順序,要保證子類靠前父類靠後的原則;此外在 java 中重寫方法丟擲的異常不能是原方法丟擲異常的父類;如果方法沒有丟擲受檢查型別異常則在呼叫方法的地方就不能主動新增受檢查型別異常捕獲,但是可以新增執行時異常或者 Exception 捕獲。

 

問:關於 java 中的異常處理你有啥心得或者經驗?

答:這其實是一個經驗題,答案不侷限的,可以自由發揮,下面給出幾個示例點。

  • 方法返回值儘量不要使用 null(特殊場景除外),這樣可以避免很多 NullPointerException 異常。

  • catch 住瞭如果真的沒必要處理則至少加行列印,這樣可在將來方便排查問題。

  • 介面方法丟擲的異常儘量保證是執行時異常型別,除非迫不得已才丟擲檢查型別異常。

  • 避免在 finally 中使用 return 語句或者丟擲異常,如果呼叫的其他程式碼可能丟擲異常則應該捕獲異常並進行處理,因為 finally 中 return 不僅會覆蓋 try 和 catch 內的返回值且還會掩蓋 try 和 catch 內的異常,就像異常沒有發生一樣(特別注意,當 try-finally 中沒有 return 時該方法執行會繼續丟擲異常)。

  • 儘量不要在 catch 塊中壓制異常(即什麼也不處理直接 return),因為這樣以後無論丟擲什麼異常都會被忽略,以至沒有留下任何問題線索,如果在這一層不知道如何處理異常最好將異常重新丟擲由上層決定如何處理異常。

  • 方法定義中 throws 後面儘量定義具體的異常列表,不要直接 throws Exception。

  • 捕獲異常時儘量捕獲具體的異常型別而不要直接捕獲其父類,這樣容易造成混亂。

  • 避免在 finally 塊中丟擲異常,不然第一個異常的呼叫棧會丟失。

  • 不要使用異常控制程式的流程,譬如本應該使用 if 語句進行條件判斷的情況下卻使用異常處理是非常不好的習慣,會嚴重影響效能。

  • 不要直接捕獲 Throwable 類,因為 Error 是 Throwable 類的子類,當應用丟擲 Errors 的時候一般都是不可恢復的情況。

當然還有其他的經驗,上面只是給出一些常見的心得經驗,具體回答時可自行拓展。

 

問:java 中 finally 塊一定會執行嗎?

答:不一定,分情況。因為首先想要執行 finally 塊的前提是必須執行到了 try 塊,當在 try 塊或者 catch 塊中有 System.exit(0); 這樣的語句存在時 finally 塊就不會被執行到了,因為程式被結束了。此外當在 try 塊或者 catch 塊裡 return 時 finally 會被執行;而且 finally 塊裡 return 語句會把 try 塊或者 catch 塊裡的 return 語句效果給覆蓋掉且吞掉了異常。