昨天在整理粉絲給我私信的時候,發現了一個挺有意思的事情。是這樣的,有一個粉絲朋友私信問我Java 的 Exception 和 Error 有什麼區別呢?說他在面試的時候被問到這個問題卡殼了,最後還好也是有驚無險的過了。在恭喜這位粉絲的同時,咱們再回過頭來這個問題,其實在面試中這是個常見的連環問題了,大多數面試官都喜歡用這個話題發問。當時看完當時內心也就下了個決心,必定要寫篇文章把 Java 的異常相關講明白,讓你們看完以後再遇到相似問題就會有所準備!java
有點 java
基礎的同窗應該都知道 throw
這個語句吧。咱們都知道throw
語句起到的做用,它會拋出一個 throwable
的子類對象,虛擬機會對這個對象進行一系列的操做,要麼能夠處理這個異常(被 catch
),或者不能處理,最終會致使語句所在的線程中止。程序員
那麼 JVM 究竟是怎麼作的呢?讓咱們一塊兒試試看吧:面試
首先寫一段代碼,throw
一個 RuntimeException
:數組
package com.company; public class TestException { public static void main(String[] args) { throw new RuntimeException(); } }
編譯後,到 class
文件所在目錄,用javap -verbose
打開 .class 文件:函數
javap -verbose TestException
能夠看到一些字節碼。咱們找到 TestException.main 函數對應的字節碼:工具
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: new #2 // class java/lang/RuntimeException 3: dup 4: invokespecial #3 // Method java/lang/RuntimeException."<init>":()V 7: athrow LineNumberTable: line 5: 0 LocalVariableTable: Start Length Slot Name Signature 0 8 0 args [Ljava/lang/String;
看 code
部分,實際上前三行就是對應 new
RuntimeExcpetion(),因爲主題緣由,這裏就不展開了。重頭戲是後面這個 athrow,它到底作了什麼呢?ui
1\. 先檢查棧頂元素,必須是一個java.lang.Throwable的子類對象的引用; 2\. 上述引用出棧,搜索本方法的異常表,是否存在處理此異常的 handler; 2.1 若是找到對應的handler,則用這個handler處理異常; 2.2 若是找不到對應的handler,當前方法棧幀出棧(退出當前方法),到調用該方法的方法中搜索異常表(重複2); 3\. 若是一直找不到 handler,當前線程終止退出,並輸出異常信息和堆棧信息(其實也就是不斷尋找 handler 的過程)。
能夠看到 throw 這個動做會形成幾個可能的反作用:this
handler
,最終會致使線程退出。好了,到這裏咱們已經搞明白 throw 到底幹了些啥。可是咱們注意到, athrow 指令尋找的是一個 java.lang.Throwable 的子類對象的引用,也就是說 throw 語句後面只能跟 java.lang.Throwable 的子類對象,不然會編譯失敗。那麼Throwable 究竟是個什麼東西呢?spa
Throwable
顧名思義,就是能夠被 throw 的對象啦!它是 java 中全部異常的父類:.net
public class Throwable implements Serializable { ... }
JDK 自帶的異常類簇,繼承關係大概是這個樣子的:
首先能夠看到 Throwable 分紅兩個大類,Exception 和 Error
Error 是 java 虛擬機拋出的錯誤,程序運行中出現的較嚴重的問題。
例如,虛擬機的堆內存不夠用了,就會拋出 OutOfMemoryError。這些異經常使用戶的代碼無需捕獲,由於捕獲了也沒用。
這就比如船壞了,而船上的乘客即使知道船壞了也沒辦法,由於這不是他們能解決的問題。
Exception 是應用程序中可能的可預測、可恢復問題。
啥意思呢?也就是說Exception都是用戶代碼層面拋出的異常。換句話說,這些異常都是船上的乘客本身能夠解決的。例如常見的空指針異常NullPointerException
,取數組下標越界時會拋出ArrayIndexOutOfBoundException
。
這些都是「乘客」的錯誤操做引起的問題,因此「乘客」是能夠解決的。
到了這裏,粉絲問到的那道面試題,是否是就已經解決了呢?
我前面也說了,這是個常見的連環問題。那麼解決了第一個問題,面試官接下來會問什麼呢?
經過上面一節的敘述你們能夠看到,就 Throwable
體系自己,與程序員關係比較大的其實仍是 Exception
及其子類。由於船上的乘客都是程序員們創造,因此他們的錯誤行爲,程序員仍是要掌握得比較透徹的。
Exception
可分爲兩種,CheckedException
和 UncheckedException
。
顧名思義,UncheckedException 也就是能夠不被檢查的異常。JVM 規定繼承自 RuntimeException 的異常都是 UncheckedException.
全部非 RuntimeException 的 Exception.
那麼問題來了,什麼叫 被檢查的異常 ? 誰檢查?
試想一下如下這個開發場景:
爲了解決這個場景中出現的問題,JVM 規定,每一個函數必須對本身要拋出的異常心中有數,在函數聲明時經過 throws 語句將該函數可能會拋出的異常聲明出來:
public Remote lookup(String name) throws RemoteException, NotBoundException, AccessException;
這個聲明就是前面說的被檢查的異常。
那麼能夠不被檢查的異常又是咋回事呢?
其實 CheckedExcpetion
之因此要被 Check
,主要仍是由於調用方是有呢你處理這些異常的。
以 java.net.URL
這個類的構造函數爲例:
public final class URL implements java.io.Serializable { ... public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException { ... if (port < -1) { throw new MalformedURLException("Invalid port number :" + port); } } }
MalformedURLException
就是一種checked exception
. 當輸入的 port < -1
時,程序就會拋出 MalformedURLException
異常,這樣調用方就能夠修正port
輸入,獲得正確的URL了。
可是有一些狀況,好比下面這個函數:
public void method(){ int [] numbers = { 1, 2, 3 }; int sum = numbers[0] + numbers[3]; }
因爲 numbers
數組只有3個元素,但函數中卻取了第4個元素,因此調用 method()
時會拋出異常 ArrayIndexOutOfBoundsException
。可是這個異常調用方是沒法修正的。
對於這種狀況,JVM 特地規定了 RuntimeException
及其子類的這種 UnchekcedException
,能夠不被 throws
語句聲明,編譯時不會報錯。
上面咱們介紹了異常的定義和拋出方式,那麼怎麼捕獲並處理異常呢?這個時候就輪到 try catch finally
出場了。舉個例子:
public void readFile(String filePath) throws FileNotFoundException { FileReader fr = null; BufferedReader br = null; try{ fr = new FileReader(filePath); br = new BufferedReader(fr); String s = ""; while((s = br.readLine()) != null){ System.out.println(s); } } catch (IOException e) { System.out.println("讀取文件時出錯: " + e.getMessage()); } finally { try { br.close(); fr.close(); } catch (IOException ex) { System.out.println("關閉文件時出錯: " + ex.getMessage()); } } }
這是一個逐行打印文件內容的函數。當輸入的 filePath
不存在時,會拋出 CheckedException
FileNotFoundException
。
在文件讀取的過程當中,也會出現一些意外狀況可能形成一些 IOException
,所以代碼對可能出現的 IOException
進行了 try catch finally
的處理。 try
代碼塊中是正常的業務代碼, catch
是對異常處理,finally
是不管try
是否異常,都要執行的代碼。對於 readFile
這個函數來講,就是要關閉文件句柄,防止內存泄漏。
這裏比較難受的是,因爲 fr br
須要在 finally
塊中執行,因此必需要在 try
前先聲明。有沒有優雅一點的寫法呢?
這裏要介紹一下 JDK 7 推出的新特性:
try-with-resources
不是一個功能,而是一套讓異常捕獲語句更加優雅的解決方案。
對於任何實現了 java.io.Closeable
接口的類,只要在 try
後面的()中初始化,JVM
都會自動增長 finally 代碼塊去執行這些 Closeable
的 close()
方法。
Closable
定義以下:
public interface Closeable extends AutoCloseable { /** * Closes this stream and releases any system resources associated * with it. If the stream is already closed then invoking this * method has no effect. * * <p> As noted in {@link AutoCloseable#close()}, cases where the * close may fail require careful attention. It is strongly advised * to relinquish the underlying resources and to internally * <em>mark</em> the {@code Closeable} as closed, prior to throwing * the {@code IOException}. * * @throws IOException if an I/O error occurs */ public void close() throws IOException; }
因爲 FileReader
和 BufferedReader
都實現了 Closeable
接口,因此上述前面咱們的 readFile
函數能夠改寫爲:
public void readFile(String filePath) throws FileNotFoundException { try( FileReader fr = new FileReader(filePath); BufferedReader br = new BufferedReader(fr) ){ String s = ""; while((s = br.readLine()) != null){ System.out.println(s); } } catch (IOException e) { System.out.println("讀取文件時出錯: " + e.getMessage()); } }
是否是清爽了許多呢?
finally,讓咱們來講一說 finally 😂。
對於一個完整的 try catch finally 代碼塊,它的執行順序是: try --> catch --> finally。
可是總有一些很奇怪的代碼,值得咱們研究一下:
public static void returnProcess() { try { return; } finally { System.out.println("Hello"); } }
你們猜猜 finally裏的代碼會不會執行呢?按道理說,先執行 try 代碼塊,直接 return 了, finally
應該不會再執行了吧?但實際狀況是, finally
中的代碼在 return
以後是會被執行的。
那再看看下面的代碼,猜猜 finally
中的代碼會不會執行:
public void exitProcess() { try { System.exit(1); } finally { System.out.println("Hello"); } }
有的同窗會說了,前面不是說了嘛,不管 try
中的代碼有沒有拋出異常, finally
中的代碼都會執行。但其實這個說法是不許確的。在本例中, try 中的代碼調用了 System.exit(1)
,這條語句會直接退出 Java
進程,進程都退出了,finally
語句代碼塊的執行機制也就不存在了 finally
中的語句也就不會執行了。
再看下面這段代碼:
public static int returnInt() { int res = 10; try { res = 30; return res; } finally { res = 50; } } public static void main(String[] args) { System.out.println(returnInt()); }
根據代碼1的經驗,finally
中的語句會執行,那 res
應該被賦值爲50了,所以程序會被輸出50
吧?
但其實並非,程序依然會被輸出 30
。
神馬!!!
這裏請你們不要站在函數的視角去看 return
語句,而要站在 JVM
視角看 return
語句。從函數視角看, return
老是最後一行執行的語句。可是站在 JVM
視角來看,它也只是一個普通的語句而已。
小調皮同窗又問了:那若是在 finally
中 return
一下會怎樣呢?
public static int returnInt() { int res = 10; try { res = 30; return res; } finally { res = 50; return res; } } public static void main(String[] args) { System.out.println(returnInt()); }
哈哈,這個時候就會輸出 50
了!
神馬鬼東西!我已凌亂在風中!
這是由於 return
語句其實是會被 「覆蓋」的。也就是說,當 finally
中出現了 return
語句時,其餘地方出現的 return
語句都無效了。而 finally
語句中的 return
語句時在 res = 50
這個賦值語句以後的,所以就返回了 50
。
因此看得出來,在 finally 代碼塊中使用 return 是個很是危險的事情:
不要在 finally 中使用 return!
但願你們看了本文之後,都有所收穫。至少在之後的面試中遇到相似問題能輕輕鬆鬆搞定,你們看完有什麼不懂的歡迎在評論區討論,或者私信問我,我通常看到後都會回的。