關於異常處理的文章已有至關的篇幅,本文簡單總結了Java的異常處理機制,並結合代碼分析了一些異常處理的最佳實踐,對異常的性能開銷進行了簡單分析。
博客另外一篇文章《[譯]Java異常處理的最佳實踐》也是關於異常處理的一篇不錯的文章。html
請思考: 對比 Exception
和 Error
,兩者有何區別? 另外,運行時異常和通常異常有什麼區別?java
首先,要明確的是 Exception
和 Error
都繼承自 Throwable
類,Java中只有 Throwable
類型的實例才能夠被拋出 (throws) 或 捕獲 (catch) ,它是異常處理機制的基本組成類型。sql
Exception
是程序正常運行中,能夠預料的意外狀況,應該被捕獲並進行相應處理。Error
是指在正常狀況下,不太可能出現的狀況,絕大多數的 Error
都會致使程序(好比 JVM 自身)處於非正常的、不可恢復狀態。既然是非正常狀況,因此不便於也不須要捕獲,常見的如 OutOfMemoryError
等,都是 Error
的子類。api
Exception
又分爲檢查型 (checked) 和 非檢查型 (unchecked) 異常,檢查型異常必須在源代碼裏顯式的進行捕獲處理,這是編譯期檢查的一部分。數組
非檢查型異常(unchecked exception) 就是所謂的運行時異常,如 NullPointerException
和 ArrayIndexOutOfBoundsException
等,一般是能夠編碼避免的邏輯錯誤,具體根據須要來判斷是否須要捕獲,並不會在編譯期強制要求。安全
下圖展現了Java中異常類繼承關係
oracle
java.lang
中定義了一些異常類,這裏只列舉其中常見的一部分,詳細查閱 java.lang.Error、java.lang.Exceptionapp
Error:分佈式
LinkageError:ide
VirtualMachineError:虛擬機錯誤。用於指示虛擬機被破壞或者繼續執行操做所需的資源不足的狀況。
Exception
檢查型異常 (checked exception)
非檢查型異常 (unchecked exception)
RuntimeException
還有一個經典的題目: NoClassDefFoundError
和 ClassNotFoundException
有什麼區別?
下面是 Throwable 類的主要方法:(java.lang.Throwable)
public String getMessage()
:返回關於發生的異常的詳細信息public Throwable getCause()
:返回一個Throwable 對象表明異常緣由public void printStackTrace()
:打印toString()結果和棧層次到System.err,即錯誤輸出流。public String toString()
:Returns a short description of this throwable.使用try
和 catch
關鍵字能夠捕獲異常。
能夠在 try
語句後面添加任意數量的 catch
塊來捕獲不一樣的異常。若是保護代碼中發生異常,異常被拋給第一個 catch
塊,若是匹配,它在這裏就會被捕獲。若是不匹配,它會被傳遞給第二個 catch
塊。如此,直到異常被捕獲或者經過全部的 catch 塊。
不管是否發生異常,finally
代碼塊中的代碼總會被執行。在 finally 代碼塊中,能夠作一些資源回收工做,如關閉JDBC鏈接。
try{ // code }catch( 異常類型1 ex ){ //.. }catch( 異常類型2 ex){ //.. }catch( 異常類型3 ex ){ //.. }finally{ //.. }
throw
的做用是拋出一個異常,不管它是新實例化的仍是剛捕獲到的。throws
是方法可能拋出異常的聲明。使用 throws
關鍵字聲明的方法表示此方法不處理異常,而交給方法調用處進行處理,一個方法能夠聲明拋出多個異常。
例如,下面的方法聲明拋出 RemoteException
和 InsufficientFundsException
:
public class className { public void withdraw(double amount) throws RemoteException, InsufficientFundsException { // Method implementation if(..) throw new RemoteException(); else throw new InsufficientFundsException(); } //Remainder of class definition }
從Java 7開始提供了兩個有用的特性:try-with-resources
和 multiple catch
。try-with-resources
將 try-catch-finally 簡化爲 try-catch,這實際上是一種語法糖,在編譯時會轉化爲 try-catch-finally 語句。自動按照約定俗成 close 那些擴展了 AutoCloseable
或者 Closeable
的對象,從而替代了finally中關閉資源的功能。如下代碼用try-with-resources
自動關閉 java.sql.Statement
:
public static void viewTable(Connection con) throws SQLException { String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES"; try (Statement stmt = con.createStatement()) { // Try-with-resources ResultSet rs = stmt.executeQuery(query); while (rs.next()) { String coffeeName = rs.getString("COF_NAME"); int supplierID = rs.getInt("SUP_ID"); float price = rs.getFloat("PRICE"); int sales = rs.getInt("SALES"); int total = rs.getInt("TOTAL"); System.out.println(coffeeName + ", " + supplierID + ", " + price + ", " + sales + ", " + total); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); } }
值得注意的是,異常拋出機制發生了變化。在過去的 try-catch-finally 結構中,若是 try
塊沒有發生異常時,直接執行finally
塊。若是try
塊發生異常,catch
塊捕捉,而後執行 finally
塊。
可是在 try-with-resources
結構中,不論 try
中是否有異常,都會首先自動執行 close
方法,而後才判斷是否進入 catch
塊。分兩種狀況討論:
try
沒有發生異常,自動調用close
方法,若是發生異常,catch 塊捕捉並處理異常。try
發生異常,而後自動調用 close
方法,若是 close
也發生異常,catch
塊只會捕捉 try
塊拋出的異常,close
方法的異常會在 catch
中被壓制,可是你能夠在 catch
塊中,用Throwable.getSuppressed
方法來獲取到壓制異常的數組。再來看看multiple catch
,當咱們須要同時捕獲多個異常,可是對這些異常處理的代碼是相同的。好比:
try { execute(); //exception might be thrown } catch (IOException ex) { LOGGER.error(ex); throw new SpecialException(); } catch (SQLException ex) { LOGGER.error(ex); throw new SpecialException(); }
使用 multiple catch
能夠把代碼寫成下面這樣:
try { execute(); //exception might be thrown } catch (IOException | SQLExceptionex ex) {// Multiple catch LOGGER.log(ex); throw new SpecialException(); }
這裏須要注意的是,上面代碼中 ex是隱式的 final
不能夠在catch
塊中改變ex。
有的時候,咱們會根據須要自定義異常。自定義的全部異常都必須是 Throwable
的子類,若是是檢查型異常,則繼承 Exception
類。若是自定義的是運行時異常,則繼承 RuntimeException
。這個時候除了保證提供足夠的信息,還有兩點須要考慮:
java.net.ConnectException
的出錯信息是"Connection refused(Connection refused)",而不包含具體的機器名、IP、端口等,一個重要考量就是信息安全。相似的狀況在日誌中也有,好比,用戶數據通常是不能夠輸出到日誌裏面的。看下面代碼,有哪些不當之處?
try { // … Thread.sleep(1000L); } catch (Exception e) { }
以上代碼雖短,但已經違反了異常處理的兩個基本原則。
第一,儘可能不要捕獲頂層的Exception,而是應該捕獲特定異常。 在這裏是 Thread.sleep()
拋出的 InterruptedException
。咱們但願本身的代碼在出現異常時可以儘可能給出詳細的異常信息,而Exception偏偏隱藏了咱們的目的,另外咱們也要保證程序不會捕獲到咱們不但願捕獲的異常,而上邊的代碼將捕獲全部的異常,包括 unchecked exception ,好比,你可能更但願RuntimeException
被擴散出來,而不是被捕獲。進一步講,儘可能不要捕獲 Throwable
或者 Error
,這樣很難保證咱們可以正確處理程序 OutOfMemoryError
。
第二,不要生吞(swallow)異常 ,這是異常處理中要特別注意的事情,由於極可能會致使很是難以診斷的詭異狀況。當try塊發生 checked exception 時,咱們應當採起一些補救措施。若是 checked exception 沒有任何意義,能夠將其轉化爲 unchecked exception 再從新拋出。千萬不要用一個空的 catch 塊捕獲來忽略它,程序可能在後續代碼以不可控的方式結束,沒有人可以輕易判斷到底是哪裏拋出了異常,以及是什麼緣由產生了異常。
try { // … } catch (IOException e) { e.printStackTrace(); }
這段在實驗中沒問題的代碼一般在產品代碼中不容許這樣處理。
查看printStackTrace()文檔開頭就是「Prints this throwable and its backtrace to the standard error stream」,問題就在這,在稍微複雜一點的生產系統中,標準出錯(STERR)不是個合適的輸出選項,由於很難判斷出到底輸出到哪裏去了。尤爲是對於分佈式系統,若是發生異常,可是沒法找到堆棧軌跡(stacktrace),這純屬是爲診斷設置障礙。因此,最好使用產品日誌,詳細地輸出到日誌系統裏。
This is probably the most famous principle about Exception handling. It basically says that you should throw an exception as soon as you can, and catch it late as much as possible. You should wait until you have all the information to handle it properly.
This principle implicitly says that you will be more likely to throw it in the low-level methods, where you will be checking if single values are null or not appropriate. And you will be making the exception climb the stack trace for quite several levels until you reach a sufficient level of abstraction to be able to handle the problem.
看下面的代碼段:
public void readPreferences(String fileName){ //...perform operations... InputStream in = new FileInputStream(fileName); //...read the preferences file... }
上段代碼中若是 fileName 爲 null
,那麼程序就會拋出 NullPointerException
,可是因爲沒有第一時間暴露出問題,堆棧信息可能很是使人費解,每每須要相對複雜的定位。在發現問題的時候,第一時間拋出,可以更加清晰地反映問題。
修改一下上面的代碼,讓問題 「throw early」,對應的異常信息就很是直觀了。
public void readPreferences(String filename) { Objects. requireNonNull(filename); // throw NullPointerException //...perform other operations... InputStream in = new FileInputStream(filename); //...read the preferences file... }
上面這段代碼使用了Objects.requireNonNull()
方法,下面是它在java.util.Objects
裏的具體實現:
public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; }
至於 catch late
,捕獲異常後,須要怎麼處理呢?最差的處理方式,就是的「生吞異常」,本質上實際上是掩蓋問題。若是實在不知道如何處理,能夠選擇保留原有異常的 cause
信息,直接再拋出或者構建新的異常拋出去。在更高層面,由於有了清晰的(業務)邏輯,每每會更清楚合適的處理方式是什麼。
從性能角度審視一下Java的異常處理機制,有兩個可能會相對昂貴的地方:
try-catch
代碼段會產生額外的性能開銷,換個角度說,它每每會影響JVM對代碼進行優化,因此建議僅捕獲有必要的代碼段,儘可能不要一個大的 try
包住整段的代碼;更不要利用異常控制代碼流程,這遠比咱們一般意義上的條件語句(if/else、switch)要低效。因此,對於部分追求極致性能的底層類庫,有種方式是嘗試建立不進行棧快照的Exception。另外,當咱們的服務出現反應變慢、吞吐量降低的時候,檢查發生最頻繁的 Exception 也是一種思路。
參考文章: