摘要:關于異常處理的文章已有相當的篇幅,本文簡單總結了的異常處理機制,并結合代碼分析了一些異常處理的最佳實踐,對異常的性能開銷進行了簡單分析。是程序正常運行中,可以預料的意外情況,應該被捕獲并進行相應處理。
關于異常處理的文章已有相當的篇幅,本文簡單總結了Java的異常處理機制,并結合代碼分析了一些異常處理的最佳實踐,對異常的性能開銷進行了簡單分析。
博客另一篇文章《[譯]Java異常處理的最佳實踐》也是關于異常處理的一篇不錯的文章。
請思考: 對比 Exception 和 Error ,二者有何區別? 另外,運行時異常和一般異常有什么區別?
Exception 和 Error 的區別首先,要明確的是 Exception 和 Error 都繼承自 Throwable 類,Java中只有 Throwable 類型的實例才可以被拋出 (throws) 或 捕獲 (catch) ,它是異常處理機制的基本組成類型。
Exception 是程序正常運行中,可以預料的意外情況,應該被捕獲并進行相應處理。
Error 是指在正常情況下,不太可能出現的情況,絕大多數的 Error 都會導致程序(比如 JVM 自身)處于非正常的、不可恢復狀態。既然是非正常情況,所以不便于也不需要捕獲,常見的如 OutOfMemoryError等,都是 Error 的子類。
Exception 又分為檢查型 (checked) 和 非檢查型 (unchecked) 異常,檢查型異常必須在源代碼里顯式的進行捕獲處理,這是編譯期檢查的一部分。
非檢查型異常(unchecked exception) 就是所謂的運行時異常,如 NullPointerException 和 ArrayIndexOutOfBoundsException 等,通常是可以編碼避免的邏輯錯誤,具體根據需要來判斷是否需要捕獲,并不會在編譯期強制要求。
Throwable、Exception、Error 的設計和分類 內置異常類下圖展示了Java中異常類繼承關系
java.lang 中定義了一些異常類,這里只列舉其中常見的一部分,詳細查閱 java.lang.Error、java.lang.Exception
Error:
LinkageError:
VirtualMachineError:虛擬機錯誤。用于指示虛擬機被破壞或者繼續執行操作所需的資源不足的情況。
OutOfMemoryError: 內存溢出錯誤
StackOverflowError:棧溢出錯誤
Exception
檢查型異常 (checked exception)
IOException
ClassNotFoundException
InstantiationException
SQLException
非檢查型異常 (unchecked exception)
RuntimeException
NullPointerException
ClassCastException
SecurityException
ArithmeticException
IndexOutOfBoundsException
還有一個經典的題目: 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-finally使用try 和 catch 關鍵字可以捕獲異常。
可以在 try 語句后面添加任意數量的 catch 塊來捕獲不同的異常。如果保護代碼中發生異常,異常被拋給第一個 catch 塊,如果匹配,它在這里就會被捕獲。如果不匹配,它會被傳遞給第二個 catch 塊。如此,直到異常被捕獲或者通過所有的 catch 塊。
無論是否發生異常,finally 代碼塊中的代碼總會被執行。在 finally 代碼塊中,可以做一些資源回收工作,如關閉JDBC連接。
try{ // code }catch( 異常類型1 ex ){ //.. }catch( 異常類型2 ex){ //.. }catch( 異常類型3 ex ){ //.. }finally{ //.. }throw、throws
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 }try-with-resources 和 multiple catch
從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。這個時候除了保證提供足夠的信息,還有兩點需要考慮:
是否需要定義成 Checked Exception,這種類型設計的初衷是為了從異常情況恢復。
在保證診斷信息足夠的同時,也要考慮避免包含敏感信息,因為那樣可能導致潛在的安全問題。例如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 staticT requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; }
至于 catch late,捕獲異常后,需要怎么處理呢?最差的處理方式,就是的“生吞異常”,本質上其實是掩蓋問題。如果實在不知道如何處理,可以選擇保留原有異常的 cause 信息,直接再拋出或者構建新的異常拋出去。在更高層面,因為有了清晰的(業務)邏輯,往往會更清楚合適的處理方式是什么。
異常處理機制的性能開銷從性能角度審視一下Java的異常處理機制,有兩個可能會相對昂貴的地方:
try-catch 代碼段會產生額外的性能開銷,換個角度說,它往往會影響JVM對代碼進行優化,所以建議僅捕獲有必要的代碼段,盡量不要一個大的 try 包住整段的代碼;更不要利用異常控制代碼流程,這遠比我們通常意義上的條件語句(if/else、switch)要低效。
Java 每實例化一個 Exception,都會對當時的棧進行快照,這是一個相對比較重的操作。如果發生的非常頻繁,這個開銷可就不能被忽略了。
所以,對于部分追求極致性能的底層類庫,有種方式是嘗試創建不進行棧快照的Exception。另外,當我們的服務出現反應變慢、吞吐量下降的時候,檢查發生最頻繁的 Exception 也是一種思路。
參考文章:
Java 異常處理 - runoob.com
Designing with exceptions :Guidelines and tips on when and how to use exceptions
Exception和Error有什么區別? - Java核心技術36講
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72623.html
摘要:本文是淺析微信支付系列文章的第六篇,主要講解支付成功后,微信回調商戶支付結果通知的處理。微信支付支付回調接口該鏈接是通過統一下單中提交的參數設置,如果鏈接無法訪問,商戶將無法接收到微信通知。 本文是【淺析微信支付】系列文章的第六篇,主要講解支付成功后,微信回調商戶支付結果通知的處理。 淺析微信支付系列已經更新五篇了喲~,沒有看過的朋友們可以看一下哦。 淺析微信支付:統一下單接口 淺析...
摘要:異常與錯誤異常是指程序運行中不符合預期情況以及與正常流程不同的狀況。在中主要的錯誤等級如下最低級別的錯誤,表示不推薦不建議。小結中錯誤和異常是兩個不同的概念,這種設計根本上導致了的異常和錯誤與其它語言相異。中,異常時錯誤唯一的報告方式。 異常與錯誤 異常是指程序運行中不符合預期情況以及與正常流程不同的狀況。錯誤則屬于自身問題,是一種非法語法或者環境問題導致的、讓編譯器無法通過檢查設置無...
摘要:廣告位出售垃圾回收機制淺析與理解對垃圾回收進行分析前,我們先來了解一些基本概念基本概念內存管理內存管理對于編程語言至關重要。里面的變量通常是局部變量函數參數等。 GC(@廣告位出售)垃圾回收機制: 淺析與理解 對垃圾回收進行分析前,我們先來了解一些基本概念 基本概念 內存管理:內存管理對于編程語言至關重要。匯編允許你操作所有東西,或者說要求你必須全權處理所有細節更合適。C 語言中雖然...
摘要:廣告位出售垃圾回收機制淺析與理解對垃圾回收進行分析前,我們先來了解一些基本概念基本概念內存管理內存管理對于編程語言至關重要。里面的變量通常是局部變量函數參數等。 GC(@廣告位出售)垃圾回收機制: 淺析與理解 對垃圾回收進行分析前,我們先來了解一些基本概念 基本概念 內存管理:內存管理對于編程語言至關重要。匯編允許你操作所有東西,或者說要求你必須全權處理所有細節更合適。C 語言中雖然...
摘要:類的定義假如要定義一個類,表示二維的坐標點最最基本的就是方法,相當于的構造函數。嚴格來講,并不支持多態。靜態類型的缺失,讓很難實現那樣嚴格的多態檢查機制。有時候,需要在子類中調用父類的方法。 類的定義 假如要定義一個類 Point,表示二維的坐標點: # point.py class Point: def __init__(self, x=0, y=0): se...
閱讀 1032·2021-11-25 09:43
閱讀 1413·2021-11-18 10:02
閱讀 1814·2021-11-02 14:41
閱讀 2366·2019-08-30 15:55
閱讀 1067·2019-08-29 16:18
閱讀 2552·2019-08-29 14:15
閱讀 1390·2019-08-26 18:13
閱讀 733·2019-08-26 10:27