摘要:關(guān)于異常處理的文章已有相當(dāng)?shù)钠疚暮唵慰偨Y(jié)了的異常處理機(jī)制,并結(jié)合代碼分析了一些異常處理的最佳實(shí)踐,對異常的性能開銷進(jìn)行了簡單分析。是程序正常運(yùn)行中,可以預(yù)料的意外情況,應(yīng)該被捕獲并進(jìn)行相應(yīng)處理。
關(guān)于異常處理的文章已有相當(dāng)?shù)钠疚暮唵慰偨Y(jié)了Java的異常處理機(jī)制,并結(jié)合代碼分析了一些異常處理的最佳實(shí)踐,對異常的性能開銷進(jìn)行了簡單分析。
博客另一篇文章《[譯]Java異常處理的最佳實(shí)踐》也是關(guān)于異常處理的一篇不錯的文章。
請思考: 對比 Exception 和 Error ,二者有何區(qū)別? 另外,運(yùn)行時異常和一般異常有什么區(qū)別?
Exception 和 Error 的區(qū)別首先,要明確的是 Exception 和 Error 都繼承自 Throwable 類,Java中只有 Throwable 類型的實(shí)例才可以被拋出 (throws) 或 捕獲 (catch) ,它是異常處理機(jī)制的基本組成類型。
Exception 是程序正常運(yùn)行中,可以預(yù)料的意外情況,應(yīng)該被捕獲并進(jìn)行相應(yīng)處理。
Error 是指在正常情況下,不太可能出現(xiàn)的情況,絕大多數(shù)的 Error 都會導(dǎo)致程序(比如 JVM 自身)處于非正常的、不可恢復(fù)狀態(tài)。既然是非正常情況,所以不便于也不需要捕獲,常見的如 OutOfMemoryError等,都是 Error 的子類。
Exception 又分為檢查型 (checked) 和 非檢查型 (unchecked) 異常,檢查型異常必須在源代碼里顯式的進(jìn)行捕獲處理,這是編譯期檢查的一部分。
非檢查型異常(unchecked exception) 就是所謂的運(yùn)行時異常,如 NullPointerException 和 ArrayIndexOutOfBoundsException 等,通常是可以編碼避免的邏輯錯誤,具體根據(jù)需要來判斷是否需要捕獲,并不會在編譯期強(qiáng)制要求。
Throwable、Exception、Error 的設(shè)計和分類 內(nèi)置異常類下圖展示了Java中異常類繼承關(guān)系
java.lang 中定義了一些異常類,這里只列舉其中常見的一部分,詳細(xì)查閱 java.lang.Error、java.lang.Exception
Error:
LinkageError:
VirtualMachineError:虛擬機(jī)錯誤。用于指示虛擬機(jī)被破壞或者繼續(xù)執(zhí)行操作所需的資源不足的情況。
OutOfMemoryError: 內(nèi)存溢出錯誤
StackOverflowError:棧溢出錯誤
Exception
檢查型異常 (checked exception)
IOException
ClassNotFoundException
InstantiationException
SQLException
非檢查型異常 (unchecked exception)
RuntimeException
NullPointerException
ClassCastException
SecurityException
ArithmeticException
IndexOutOfBoundsException
還有一個經(jīng)典的題目: NoClassDefFoundError 和 ClassNotFoundException 有什么區(qū)別?
異常方法下面是 Throwable 類的主要方法:(java.lang.Throwable)
public String getMessage() :返回關(guān)于發(fā)生的異常的詳細(xì)信息
public Throwable getCause():返回一個Throwable 對象代表異常原因
public void printStackTrace():打印toString()結(jié)果和棧層次到System.err,即錯誤輸出流。
public String toString():Returns a short description of this throwable.
捕獲、拋出異常 try-catch-finally使用try 和 catch 關(guān)鍵字可以捕獲異常。
可以在 try 語句后面添加任意數(shù)量的 catch 塊來捕獲不同的異常。如果保護(hù)代碼中發(fā)生異常,異常被拋給第一個 catch 塊,如果匹配,它在這里就會被捕獲。如果不匹配,它會被傳遞給第二個 catch 塊。如此,直到異常被捕獲或者通過所有的 catch 塊。
無論是否發(fā)生異常,finally 代碼塊中的代碼總會被執(zhí)行。在 finally 代碼塊中,可以做一些資源回收工作,如關(guān)閉JDBC連接。
try{ // code }catch( 異常類型1 ex ){ //.. }catch( 異常類型2 ex){ //.. }catch( 異常類型3 ex ){ //.. }finally{ //.. }throw、throws
throw 的作用是拋出一個異常,無論它是新實(shí)例化的還是剛捕獲到的。
throws 是方法可能拋出異常的聲明。使用 throws 關(guān)鍵字聲明的方法表示此方法不處理異常,而交給方法調(diào)用處進(jìn)行處理,一個方法可以聲明拋出多個異常。
例如,下面的方法聲明拋出 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,這其實(shí)是一種語法糖,在編譯時會轉(zhuǎn)化為 try-catch-finally 語句。自動按照約定俗成 close 那些擴(kuò)展了 AutoCloseable 或者 Closeable 的對象,從而替代了finally中關(guān)閉資源的功能。以下代碼用try-with-resources 自動關(guān)閉 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); } }
值得注意的是,異常拋出機(jī)制發(fā)生了變化。在過去的 try-catch-finally 結(jié)構(gòu)中,如果 try 塊沒有發(fā)生異常時,直接執(zhí)行finally塊。如果try 塊發(fā)生異常,catch 塊捕捉,然后執(zhí)行 finally 塊。
但是在 try-with-resources 結(jié)構(gòu)中,不論 try 中是否有異常,都會首先自動執(zhí)行 close 方法,然后才判斷是否進(jìn)入 catch塊。分兩種情況討論:
try 沒有發(fā)生異常,自動調(diào)用close方法,如果發(fā)生異常,catch 塊捕捉并處理異常。
try 發(fā)生異常,然后自動調(diào)用 close 方法,如果 close 也發(fā)生異常,catch 塊只會捕捉 try 塊拋出的異常,close 方法的異常會在 catch 中被壓制,但是你可以在 catch 塊中,用Throwable.getSuppressed 方法來獲取到壓制異常的數(shù)組。
再來看看multiple catch ,當(dāng)我們需要同時捕獲多個異常,但是對這些異常處理的代碼是相同的。比如:
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。
自定義異常有的時候,我們會根據(jù)需要自定義異常。自定義的所有異常都必須是 Throwable 的子類,如果是檢查型異常,則繼承 Exception 類。如果自定義的是運(yùn)行時異常,則繼承 RuntimeException。這個時候除了保證提供足夠的信息,還有兩點(diǎn)需要考慮:
是否需要定義成 Checked Exception,這種類型設(shè)計的初衷是為了從異常情況恢復(fù)。
在保證診斷信息足夠的同時,也要考慮避免包含敏感信息,因?yàn)槟菢涌赡軐?dǎo)致潛在的安全問題。例如java.net.ConnectException的出錯信息是"Connection refused(Connection refused)",而不包含具體的機(jī)器名、IP、端口等,一個重要考量就是信息安全。類似的情況在日志中也有,比如,用戶數(shù)據(jù)一般是不可以輸出到日志里面的。
異常處理的最佳實(shí)踐看下面代碼,有哪些不當(dāng)之處?
try { // … Thread.sleep(1000L); } catch (Exception e) { }
以上代碼雖短,但已經(jīng)違反了異常處理的兩個基本原則。
第一,盡量不要捕獲頂層的Exception,而是應(yīng)該捕獲特定異常。 在這里是 Thread.sleep() 拋出的 InterruptedException。我們希望自己的代碼在出現(xiàn)異常時能夠盡量給出詳細(xì)的異常信息,而Exception恰恰隱藏了我們的目的,另外我們也要保證程序不會捕獲到我們不希望捕獲的異常,而上邊的代碼將捕獲所有的異常,包括 unchecked exception ,比如,你可能更希望
RuntimeException 被擴(kuò)散出來,而不是被捕獲。進(jìn)一步講,盡量不要捕獲 Throwable 或者 Error,這樣很難保證我們能夠正確處理程序 OutOfMemoryError。
第二,不要生吞(swallow)異常 ,這是異常處理中要特別注意的事情,因?yàn)楹芸赡軙?dǎo)致非常難以診斷的詭異情況。當(dāng)try塊發(fā)生 checked exception 時,我們應(yīng)當(dāng)采取一些補(bǔ)救措施。如果 checked exception 沒有任何意義,可以將其轉(zhuǎn)化為 unchecked exception 再重新拋出。千萬不要用一個空的 catch 塊捕獲來忽略它,程序可能在后續(xù)代碼以不可控的方式結(jié)束,沒有人能夠輕易判斷究竟是哪里拋出了異常,以及是什么原因產(chǎn)生了異常。
try { // … } catch (IOException e) { e.printStackTrace(); }
這段在實(shí)驗(yàn)中沒問題的代碼通常在產(chǎn)品代碼中不允許這樣處理。
查看printStackTrace()文檔開頭就是“Prints this throwable and its backtrace to the standard error stream”,問題就在這,在稍微復(fù)雜一點(diǎn)的生產(chǎn)系統(tǒng)中,標(biāo)準(zhǔn)出錯(STERR)不是個合適的輸出選項(xiàng),因?yàn)楹茈y判斷出到底輸出到哪里去了。尤其是對于分布式系統(tǒng),如果發(fā)生異常,但是無法找到堆棧軌跡(stacktrace),這純屬是為診斷設(shè)置障礙。所以,最好使用產(chǎn)品日志,詳細(xì)地輸出到日志系統(tǒng)里。
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,但是由于沒有第一時間暴露出問題,堆棧信息可能非常令人費(fèi)解,往往需要相對復(fù)雜的定位。在發(fā)現(xiàn)問題的時候,第一時間拋出,能夠更加清晰地反映問題。
修改一下上面的代碼,讓問題 “throw early”,對應(yīng)的異常信息就非常直觀了。
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里的具體實(shí)現(xiàn):
public staticT requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; }
至于 catch late,捕獲異常后,需要怎么處理呢?最差的處理方式,就是的“生吞異?!?,本質(zhì)上其實(shí)是掩蓋問題。如果實(shí)在不知道如何處理,可以選擇保留原有異常的 cause 信息,直接再拋出或者構(gòu)建新的異常拋出去。在更高層面,因?yàn)橛辛饲逦模I(yè)務(wù))邏輯,往往會更清楚合適的處理方式是什么。
異常處理機(jī)制的性能開銷從性能角度審視一下Java的異常處理機(jī)制,有兩個可能會相對昂貴的地方:
try-catch 代碼段會產(chǎn)生額外的性能開銷,換個角度說,它往往會影響JVM對代碼進(jìn)行優(yōu)化,所以建議僅捕獲有必要的代碼段,盡量不要一個大的 try 包住整段的代碼;更不要利用異??刂拼a流程,這遠(yuǎn)比我們通常意義上的條件語句(if/else、switch)要低效。
Java 每實(shí)例化一個 Exception,都會對當(dāng)時的棧進(jìn)行快照,這是一個相對比較重的操作。如果發(fā)生的非常頻繁,這個開銷可就不能被忽略了。
所以,對于部分追求極致性能的底層類庫,有種方式是嘗試創(chuàng)建不進(jìn)行??煺盏腅xception。另外,當(dāng)我們的服務(wù)出現(xiàn)反應(yīng)變慢、吞吐量下降的時候,檢查發(fā)生最頻繁的 Exception 也是一種思路。
參考文章:
Java 異常處理 - runoob.com
Designing with exceptions :Guidelines and tips on when and how to use exceptions
Exception和Error有什么區(qū)別? - Java核心技術(shù)36講
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/72623.html
摘要:本文是淺析微信支付系列文章的第六篇,主要講解支付成功后,微信回調(diào)商戶支付結(jié)果通知的處理。微信支付支付回調(diào)接口該鏈接是通過統(tǒng)一下單中提交的參數(shù)設(shè)置,如果鏈接無法訪問,商戶將無法接收到微信通知。 本文是【淺析微信支付】系列文章的第六篇,主要講解支付成功后,微信回調(diào)商戶支付結(jié)果通知的處理。 淺析微信支付系列已經(jīng)更新五篇了喲~,沒有看過的朋友們可以看一下哦。 淺析微信支付:統(tǒng)一下單接口 淺析...
摘要:異常與錯誤異常是指程序運(yùn)行中不符合預(yù)期情況以及與正常流程不同的狀況。在中主要的錯誤等級如下最低級別的錯誤,表示不推薦不建議。小結(jié)中錯誤和異常是兩個不同的概念,這種設(shè)計根本上導(dǎo)致了的異常和錯誤與其它語言相異。中,異常時錯誤唯一的報告方式。 異常與錯誤 異常是指程序運(yùn)行中不符合預(yù)期情況以及與正常流程不同的狀況。錯誤則屬于自身問題,是一種非法語法或者環(huán)境問題導(dǎo)致的、讓編譯器無法通過檢查設(shè)置無...
摘要:廣告位出售垃圾回收機(jī)制淺析與理解對垃圾回收進(jìn)行分析前,我們先來了解一些基本概念基本概念內(nèi)存管理內(nèi)存管理對于編程語言至關(guān)重要。里面的變量通常是局部變量函數(shù)參數(shù)等。 GC(@廣告位出售)垃圾回收機(jī)制: 淺析與理解 對垃圾回收進(jìn)行分析前,我們先來了解一些基本概念 基本概念 內(nèi)存管理:內(nèi)存管理對于編程語言至關(guān)重要。匯編允許你操作所有東西,或者說要求你必須全權(quán)處理所有細(xì)節(jié)更合適。C 語言中雖然...
摘要:廣告位出售垃圾回收機(jī)制淺析與理解對垃圾回收進(jìn)行分析前,我們先來了解一些基本概念基本概念內(nèi)存管理內(nèi)存管理對于編程語言至關(guān)重要。里面的變量通常是局部變量函數(shù)參數(shù)等。 GC(@廣告位出售)垃圾回收機(jī)制: 淺析與理解 對垃圾回收進(jìn)行分析前,我們先來了解一些基本概念 基本概念 內(nèi)存管理:內(nèi)存管理對于編程語言至關(guān)重要。匯編允許你操作所有東西,或者說要求你必須全權(quán)處理所有細(xì)節(jié)更合適。C 語言中雖然...
摘要:類的定義假如要定義一個類,表示二維的坐標(biāo)點(diǎn)最最基本的就是方法,相當(dāng)于的構(gòu)造函數(shù)。嚴(yán)格來講,并不支持多態(tài)。靜態(tài)類型的缺失,讓很難實(shí)現(xiàn)那樣嚴(yán)格的多態(tài)檢查機(jī)制。有時候,需要在子類中調(diào)用父類的方法。 類的定義 假如要定義一個類 Point,表示二維的坐標(biāo)點(diǎn): # point.py class Point: def __init__(self, x=0, y=0): se...
閱讀 1056·2021-11-25 09:43
閱讀 1426·2021-11-18 10:02
閱讀 1869·2021-11-02 14:41
閱讀 2381·2019-08-30 15:55
閱讀 1080·2019-08-29 16:18
閱讀 2564·2019-08-29 14:15
閱讀 1400·2019-08-26 18:13
閱讀 746·2019-08-26 10:27