問:java 異常有哪幾種,特點是什么?
答:異常是發生在程序執行過程中阻礙程序正常執行的錯誤操作,只要在 Java 語句執行中產生異常則一個異常對象就會被創建。Throwable 是所有異常的父類,它有兩個直接子類 Error 和 Exception,其中 Exception 又被繼續划分為被檢查的異常(checked exception)和運行時的異常(runtime exception,即不受檢查的異常);Error 表示系統錯誤,通常不能預期和恢復(譬如 JVM 崩潰、內存不足等);被檢查的異常(Checked exception)在程序中能預期且要嘗試修復(如我們必須捕獲 FileNotFoundException 異常並為用戶提供有用信息和合適日志來進行調試,Exception 是所有被檢查的異常的父類);運行時異常(Runtime Exception)又稱為不受檢查異常,譬如我們檢索數組元素之前必須確認數組的長度,否則就可能會拋出 ArrayIndexOutOfBoundException 運行時異常,RuntimeException 是所有運行時異常的父類。
問:java 中 throw 與 throws 的區別是什么?
答:a、使用位置; b、
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() 方法來訪問異常最終的根源。
有的時候我們會用printStackTrace來打印異常棧,有可能我們會在處理異常的時候同時又拋出一個異常。 自定義兩個異常 class MyException1 extends Exception{ } class MyException2 extends Exception{ MyException2(Throwable throwable){ super(throwable); } MyException2(){ super(); } } MyException2 調用了父類的構造方法,目的是為了能夠傳遞一個cause進來。 接下來定義一個A 類,並在他的f()方法里調用g()方法,然后在f處理g里拋出的異常的時候再次拋出一個異常 class A{ public void f() throws MyException2{ try { g(); } catch (MyException1 e) { // TODO Auto-generated catch block e.printStackTrace(); throw new MyException2(); } } public void g() throws MyException1{ throw new MyException1(); } } 在catch里我們又拋出了一個MyException2的異常,這里我們先調用它的無參構造方法 接下來我們在main里跑一下 public class Main { public static void main(String[] args) { A a = new A(); try { a.f(); } catch (MyException2 e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 看一下控制台 我們可以明顯的發現異常棧的內容變少了。這是因為MyException2對MyException1的環境一無所知。 那么如果MyException2 能夠持有MyException1的環境信息,這樣做起來就十分方便了,並且也形成了一條鏈,我們也成為異常鏈。 唯一要修改的就是調用MyException2的有參構造函數,把MyException1當作cause傳遞進去,這樣的話我們就能獲取到MyException1的信息了。 class A{ public void f() throws MyException2{ try { g(); } catch (MyException1 e) { // TODO Auto-generated catch block e.printStackTrace(); //這里做了修改 throw new MyException2(e); } } public void g() throws MyException1{ throw new MyException1(); } } 我們再次運行 我們會發現我們已經可以持有MyException1的環境信息了
問: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"); }
/**輸出
Exception in thread "main" java.lang.RuntimeException: hello
at javabasics.exception.ExcepyionDemo.test3(ExcepyionDemo.java:12)
at javabasics.exception.ExcepyionDemo.main(ExcepyionDemo.java:18)
Process finished with exit code 1
*/ }
答:本題旨在考察 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) {//Exception 'java.lang.RuntimeException' has already been caught 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 {
//'func()' in 'javabasics.exception.ExcepyionDemo.Sub' clashes with 'func()' in 'javabasics.exception.ExcepyionDemo.Base'; overridden method does not throw 'java.lang.Exception'
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 語句效果給覆蓋掉且吞掉了異常。
問:java 中什么時候使用斷言(assert)?
答:斷言在開發中是一種常用的調試方式,很多開發語言中都支持這種機制。一般來說,斷言用於保證程序最基本、關鍵的正確性,斷言檢查通常在開發和測試時開啟,為了保證程序的執行效率,在軟件發布后斷言檢查通常是關閉的,斷言是一個包含布爾表達式的語句,在執行這個語句時假定該表達式為 true,如果表達式的值為 false 則系統會報告一個 AssertionError。