Java中的異常處理:高級特性和類型
譯文譯者 | 李睿
審校 | 重樓
Java平臺包含了多種語言特性和庫類型,用于處理與預(yù)期程序行為不同的異常。本文將介紹Java中異常處理的高級特性,其中包括堆棧跟蹤(stack traces)、異常鏈(exception chaining)、try-with-resources、多重捕獲(multi-catch)、最終重新拋出異常(final re-throw),以及堆棧遍歷(stack walking)。
Java教程的學(xué)習(xí)內(nèi)容
以下將介紹在Java程序中處理異常的高級特性:
- 堆棧跟蹤和堆棧跟蹤元素
- 原因(Causes)和異常鏈(exception chaining)
- Try-with-resources
- 多重捕獲(multi-catch)
- 最終重新拋出異常(final re-throw)
- StackWalking和StackWalking API
使用堆棧跟蹤進(jìn)行異常處理
每個JVM線程(執(zhí)行路徑)都與創(chuàng)建線程時創(chuàng)建的堆棧相關(guān)聯(lián)。這個數(shù)據(jù)結(jié)構(gòu)被劃分為幀(frame),這些幀是與方法調(diào)用相關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)。因此,每個線程的堆棧通常被稱為方法調(diào)用堆棧。
每當(dāng)調(diào)用一個方法時,就會創(chuàng)建一個新的幀。每幀存儲局部變量、參數(shù)變量(保存?zhèn)鬟f給方法的參數(shù))、返回到調(diào)用方法的信息、存儲返回值的空間、在調(diào)度異常時有用的信息等等。
堆棧跟蹤是在線程執(zhí)行過程中某個時間點的活動堆棧幀的報告。Java的Throwable類(在Java.lang包中)提供了打印堆棧跟蹤、填充堆棧跟蹤和訪問堆棧跟蹤元素的方法。
下載本教程中示例的源代碼。
打印堆棧跟蹤
當(dāng)throw語句拋出一個throwable時,它首先會在當(dāng)前執(zhí)行的方法中查找一個合適的catch塊。如果沒有找到,它會回溯方法調(diào)用堆棧,尋找能夠處理該異常的最近的catch塊。如果仍然沒有找到,JVM將終止執(zhí)行,并顯示一條合適的消息??梢栽?/span>清單1中看到這一行為。
清單1.PrintStackTraceDemo.java (version 1)
import java.io.IOException;
public class PrintStackTraceDemo
{
public static void main(String[] args) throws IOException
{
throw new IOException();
}
}
清單1的示例創(chuàng)建了一個java.io.IOException對象,并將該對象拋出main()方法。因為main()不處理這個throwable,而且main()是頂級方法,因此JVM會終止執(zhí)行,并顯示一條合適的消息。對于這個應(yīng)用程序,將看到以下消息:
Exception in thread "main" java.io.IOException
at PrintStackTraceDemo.main(PrintStackTraceDemo.java:7)
JVM通過調(diào)用Throwable的void printStackTrace()方法輸出這條消息,該方法在標(biāo)準(zhǔn)錯誤流上打印調(diào)用throwable對象的堆棧跟蹤。輸出的第一行顯示了調(diào)用throwable對象的toString()方法的結(jié)果。下一行顯示了fillInStackTrace()之前記錄的數(shù)據(jù)(稍后討論)。
其他的打印堆棧跟蹤方法
throwable的重載void printStackTrace(PrintStream ps)和void printStackTrace(printwwriter pw)方法將堆棧跟蹤輸出到指定的流或?qū)懭肫鳌?/span>
堆棧跟蹤顯示了創(chuàng)建throwable的源文件和行號。在本例中,它是在PrintStackTrace.java源文件的第7行創(chuàng)建的。
可以直接調(diào)用printStackTrace(),通常是調(diào)用catch塊。例如,考慮PrintStackTraceDemo應(yīng)用程序的第二個版本。
清單2. PrintStackTraceDemo.java (version 2)
import java.io.IOException;
public class PrintStackTraceDemo
{
public static void main(String[] args) throws IOException
{
try
{
a();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
static void a() throws IOException
{
b();
}
static void b() throws IOException
{
throw new IOException();
}
}
清單2展示了一個main()方法,它調(diào)用方法a(),而方法a()又調(diào)用方法b()。方法b()向JVM拋出一個IOException對象,JVM將展開方法調(diào)用堆棧,直到找到main()的catch塊,該塊可以處理異常。異常是通過對throwable調(diào)用printStackTrace()來處理的。這種方法產(chǎn)生以下輸出:
java.io.IOException
at PrintStackTraceDemo.b(PrintStackTraceDemo.java:24)
at PrintStackTraceDemo.a(PrintStackTraceDemo.java:19)
at PrintStackTraceDemo.main(PrintStackTraceDemo.java:9)
printStackTrace()不會輸出線程的名稱。與其相反,它首先會在第一行調(diào)用 Throwable 對象的 toString() 方法,以返回異常的完全限定類名(如 java.io.IOException),這是輸出的第一部分。然后輸出方法調(diào)用層次結(jié)構(gòu):最近調(diào)用的方法(b())位于頂部,main()位于底部。
堆棧跟蹤標(biāo)識的是哪一行?
堆棧跟蹤標(biāo)識了創(chuàng)建throwable的行,但它不會標(biāo)識拋出throwable的行(通過throw),除非拋出和創(chuàng)建是在同一行代碼中完成的。
填充堆棧跟蹤
throwable聲明了一個Throwable fillInStackTrace()方法來填充執(zhí)行堆棧跟蹤。在調(diào)用Throwable對象中,它記錄有關(guān)當(dāng)前線程堆棧幀的當(dāng)前狀態(tài)的信息??紤]清單3。
清單3. FillInStackTraceDemo.java (version 1)
import java.io.IOException;
public class FillInStackTraceDemo
{
public static void main(String[] args) throws IOException
{
try
{
a();
}
catch (IOException ioe)
{
ioe.printStackTrace();
System.out.println();
throw (IOException) ioe.fillInStackTrace();
}
}
static void a() throws IOException
{
b();
}
static void b() throws IOException
{
throw new IOException();
}
}
清單3和清單2的主要區(qū)別在于catch塊的throw (IOException) ioe.fillInStackTrace();聲明。該語句替換ioe的堆棧跟蹤,然后重新拋出throwable。應(yīng)該看到這樣的輸出:
java.io.IOException
at FillInStackTraceDemo.b(FillInStackTraceDemo.java:26)
at FillInStackTraceDemo.a(FillInStackTraceDemo.java:21)
at FillInStackTraceDemo.main(FillInStackTraceDemo.java:9)
Exception in thread "main" java.io.IOException
at FillInStackTraceDemo.main(FillInStackTraceDemo.java:15)
第二個堆棧跟蹤顯示ioe.fillInStackTrace()的位置,而不是重復(fù)標(biāo)識IOException對象創(chuàng)建位置的初始堆棧跟蹤。
Throwable構(gòu)造函數(shù)和fillInStackTrace()
throwable的每個構(gòu)造函數(shù)都調(diào)用fillInStackTrace()。然而,當(dāng)將false傳遞給writableStackTrace時,下面的構(gòu)造函數(shù)(在JDK 7中引入)不會調(diào)用這個方法:
Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
fillInStackTrace()調(diào)用一個原生方法,該方法沿著當(dāng)前線程的方法調(diào)用堆棧遍歷以構(gòu)建堆棧跟蹤。這種遍歷代價高昂,如果過于頻繁,可能會影響性能。
如果遇到性能非常關(guān)鍵的情況(可能涉及嵌入式設(shè)備),可以通過重寫fillInStackTrace()來阻止堆棧跟蹤的構(gòu)建。請查看清單4。
清單4. FillInStackTraceDemo.java (version 2)
{
public static void main(String[] args) throws NoStackTraceException
{
try
{
a();
}
catch (NoStackTraceException nste)
{
nste.printStackTrace();
}
}
static void a() throws NoStackTraceException
{
b();
}
static void b() throws NoStackTraceException
{
throw new NoStackTraceException();
}
}
class NoStackTraceException extends Exception
{
@Override
public synchronized Throwable fillInStackTrace()
{
return this;
}
}
清單4引入了NoStackTraceException。這個自定義檢查的異常類重寫fillInStackTrace()以返回This——對調(diào)用Throwable的引用。該程序生成以下輸出:
NoStackTraceException
注釋掉覆蓋的fillInStackTrace()方法,會看到以下輸出:
NoStackTraceException
at FillInStackTraceDemo.b(FillInStackTraceDemo.java:22)
at FillInStackTraceDemo.a(FillInStackTraceDemo.java:17)
at FillInStackTraceDemo.main(FillInStackTraceDemo.java:7)
訪問堆棧跟蹤的元素
有時,需要訪問堆棧跟蹤的元素,以便提取日志記錄、識別資源泄漏源和其他目的所需的詳細(xì)信息。printStackTrace()和fillInStackTrace()方法不支持這個任務(wù),但是java.lang.StackTraceElement和它的方法就是為這個任務(wù)設(shè)計的。
stacktraceelement類描述了在堆棧跟蹤中表示堆棧幀的元素。它的方法可用于返回類的完全限定名,該類包含這個堆棧跟蹤元素所表示的執(zhí)行點以及其他有用信息。以下是該類的主要方法:
- String getClassName()返回包含這個堆棧跟蹤元素所表示的執(zhí)行點的類的完全限定名。
- String getFileName()返回包含這個堆棧跟蹤元素所表示的執(zhí)行點的源文件的名稱。
- int getLineNumber()返回包含這個堆棧跟蹤元素所表示的執(zhí)行點的源行的行號。
- String getMethodName()返回包含這個堆棧跟蹤元素所表示的執(zhí)行點的方法的名稱。
- 當(dāng)包含這個堆棧跟蹤元素所表示的執(zhí)行點的方法是原生方法時,boolean isNativeMethod() 返回true。
另一個重要方法是java.lang.Thread和Throwable類上的StackTraceElement[]getStackTrace()。這個方法分別返回一個堆棧跟蹤元素數(shù)組,表示調(diào)用線程的堆棧轉(zhuǎn)儲,并提供對printStackTrace()打印的堆棧跟蹤信息的程序化訪問。
清單5展示了StackTraceElement和getStackTrace()。
清單5. StackTraceElementDemo.java (version 1)
import java.io.IOException;
public class StackTraceElementDemo
{
public static void main(String[] args) throws IOException
{
try
{
a();
}
catch (IOException ioe)
{
StackTraceElement[] stackTrace = ioe.getStackTrace();
for (int i = 0; i < stackTrace.length; i++)
{
System.err.println("Exception thrown from " +
stackTrace[i].getMethodName() + " in class " +
stackTrace[i].getClassName() + " on line " +
stackTrace[i].getLineNumber() + " of file " +
stackTrace[i].getFileName());
System.err.println();
}
}
}
static void a() throws IOException
{
b();
}
static void b() throws IOException
{
throw new IOException();
}
}
當(dāng)運行這個應(yīng)用程序時,會看到以下輸出:
Exception thrown from b in class StackTraceElementDemo on line 33 of file StackTraceElementDemo.java
Exception thrown from a in class StackTraceElementDemo on line 28 of file StackTraceElementDemo.java
Exception thrown from main in class StackTraceElementDemo on line 9 of file StackTraceElementDemo.java
最后,在Throwable類上有setStackTrace()方法。這種方法是為遠(yuǎn)程過程調(diào)用(RPC)框架和其他高級系統(tǒng)設(shè)計的,允許客戶端覆蓋在構(gòu)造Throwable時由fillInStackTrace()生成的默認(rèn)堆棧跟蹤。
之前展示了如何重寫fillInStackTrace()以防止構(gòu)建堆棧跟蹤。然而,可以使用StackTraceElement和setStackTrace()來安裝新的堆棧跟蹤。可以創(chuàng)建一個通過以下構(gòu)造函數(shù)初始化的StackTraceElement對象數(shù)組,并將該數(shù)組傳遞給setStackTrace():
StackTraceElement(String declaringClass, String methodName, String fileName, int lineNumber)
清單6演示了StackTraceElement和setStackTrace()。
清單6 . StackTraceElementDemo.java (version 2)
public class StackTraceElementDemo
{
public static void main(String[] args) throws NoStackTraceException
{
try
{
a();
}
catch (NoStackTraceException nste)
{
nste.printStackTrace();
}
}
static void a() throws NoStackTraceException
{
b();
}
static void b() throws NoStackTraceException
{
throw new NoStackTraceException();
}
}
class NoStackTraceException extends Exception
{
@Override
public synchronized Throwable fillInStackTrace()
{
setStackTrace(new StackTraceElement[]
{
new StackTraceElement("*StackTraceElementDemo*",
"b()",
"StackTraceElementDemo.java",
22),
new StackTraceElement("*StackTraceElementDemo*",
"a()",
"StackTraceElementDemo.java",
17),
new StackTraceElement("*StackTraceElementDemo*",
"main()",
"StackTraceElementDemo.java",
7)
});
return this;
}
}
采用星號包圍了StackTraceElementDemo類名,以證明這個堆棧跟蹤是輸出的跟蹤。運行應(yīng)用程序,將觀察到以下堆棧跟蹤:
NoStackTraceException
at *StackTraceElementDemo*.b()(StackTraceElementDemo.java:22)
at *StackTraceElementDemo*.a()(StackTraceElementDemo.java:17)
at *StackTraceElementDemo*.main()(StackTraceElementDemo.java:7)
原因和異常鏈
在異常處理中,一個catch塊經(jīng)常會通過拋出另一個異常來響應(yīng)捕獲到的異常。第一個異常被認(rèn)為是導(dǎo)致第二個異常發(fā)生的原因,這兩個異常被隱式地鏈接在一起。
例如,調(diào)用庫方法的catch塊時可能會出現(xiàn)內(nèi)部異常,而該內(nèi)部異常對標(biāo)準(zhǔn)庫方法的調(diào)用者不應(yīng)該是可見的。因為需要通知調(diào)用者出現(xiàn)了錯誤,所以catch塊會創(chuàng)建一個符合庫方法合約接口的外部異常,并且調(diào)用者可以處理這個異常。
由于內(nèi)部異常可能對于診斷問題非常有幫助,catch塊應(yīng)該將內(nèi)部異常包裝在外部異常中。被包裝的異常被稱為“原因”(cause),因為它的存在導(dǎo)致拋出外部異常。此外,包裝的內(nèi)部異常(原因)被顯式地鏈接到外部異常。
對原因和異常鏈的支持是通過兩個Throwable構(gòu)造函數(shù)(以及對應(yīng)的exception、RuntimeException和Error)和兩種方法提供的:
- Throwable(String message, Throwable cause)
- Throwable(Throwable cause)
- Throwable getCause()
- Throwable initCause(Throwable cause)
Throwable構(gòu)造函數(shù)(及其對應(yīng)的子類)允許在構(gòu)造Throwable時包裝原因。如果處理的教學(xué)法遺留代碼的自定義異常類不支持任何一個構(gòu)造函數(shù),可以通過在Throwable上調(diào)用initCause()來包裝原因。需要注意的是,initCause()只能被調(diào)用一次。無論哪種方式,都可以通過調(diào)用getCause()返回原因。當(dāng)沒有原因時,這個方法返回null。
清單7 展示了一個名為 CauseDemo 的應(yīng)用程序,該應(yīng)用程序演示了異常原因(以及異常鏈)的概念。
清單7. CauseDemo.java
public class CauseDemo
{
public static void main(String[] args)
{
try
{
Library.externalOp();
}
catch (ExternalException ee)
{
ee.printStackTrace();
}
}
}
class Library
{
static void externalOp() throws ExternalException
{
try
{
throw new InternalException();
}
catch (InternalException ie)
{
throw (ExternalException) new ExternalException().initCause(ie);
}
}
private static class InternalException extends Exception
{
}
}
class ExternalException extends Exception
{
}
清單7顯示了CauseDemo、Library和ExternalException類。CauseDemo的main()方法調(diào)用Library的externalOp()方法并catch其拋出的ExternalException對象。catch塊調(diào)用printStackTrace()來輸出外部異常及其原因。
庫的externalOp()方法故意拋出一個InternalException對象,其catch塊將該對象映射到一個ExternalException對象。因為ExternalException不支持可以接受cause參數(shù)的構(gòu)造函數(shù),所以使用initCause()來包裝InternalException對象。
運行這個應(yīng)用程序,就會看到下面的堆棧跟蹤:
ExternalException
at Library.externalOp(CauseDemo.java:26)
at CauseDemo.main(CauseDemo.java:7)
Caused by: Library$InternalException
at Library.externalOp(CauseDemo.java:22)
... 1 more
第一個堆棧跟蹤顯示,外部異常起源于Library的externalOp()方法(CauseDemo.java中的第26行),并在第7行對該方法的調(diào)用中拋出。第二個堆棧跟蹤顯示了內(nèi)部異常原因源自Library的externalOp()方法(CauseDemo.java中的第22行)。... 1 more行表示第一個堆棧跟蹤的最后一行。如果可以刪除這一行,將看到以下輸出:
ExternalException
at Library.externalOp(CauseDemo.java:26)
at CauseDemo.main(CauseDemo.java:7)
Caused by: Library$InternalException
at Library.externalOp(CauseDemo.java:22)
at CauseDemo.main(CauseDemo.java:7)
可以通過改變ee.printStackTrace(); to來證明第二個跟蹤的最后一行是第一個堆棧跟蹤的最后一行的副本。
ee.getCause().printStackTrace();.
關(guān)于“...more””以及探究原因鏈的更多信息
通常,“...n more”這樣的表述意味著原因的堆棧跟蹤的最后n行與前一個堆棧跟蹤的最后n行是重復(fù)的。
這個例子只揭示了一個原因。然而,從非簡單的現(xiàn)實世界應(yīng)用程序中拋出的異??赡馨啥鄠€原因構(gòu)成的復(fù)雜鏈??梢酝ㄟ^使用如下所示的循環(huán)來訪問這些原因:
catch (Exception exc)
{
Throwable t = exc.getCause();
while (t != null)
{
System.out.println(t);
t = t.getCause();
}
}
Try-with-resources
Java應(yīng)用程序經(jīng)常訪問文件、數(shù)據(jù)庫連接、套接字和其他依賴于相關(guān)系統(tǒng)資源的資源(例如文件句柄)。系統(tǒng)資源的稀缺性意味著它們最終必須被釋放,即使在發(fā)生異常時也是如此。當(dāng)系統(tǒng)資源沒有被釋放,應(yīng)用程序在嘗試獲取其他資源時最終會失敗,因為沒有更多相關(guān)的系統(tǒng)資源可用。
在對異常處理基礎(chǔ)的介紹中,提到資源(實際上是它們所依賴的系統(tǒng)資源)在finally塊中釋放。這可能會導(dǎo)致冗長的樣板代碼,例如下面顯示的文件關(guān)閉代碼:
finally
{
if (fis != null)
try
{
fis.close();
}
catch (IOException ioe)
{
// ignore exception
}
if (fos != null)
try
{
fos.close();
}
catch (IOException ioe)
{
// ignore exception
}
}
這個樣板代碼不僅增加了類文件的容量,而且編寫它的單調(diào)乏味可能會導(dǎo)致錯誤,甚至可能無法關(guān)閉文件。JDK 7引入了“try-with-resource”來克服這個問題。
try-with-resource的基本原理
當(dāng)執(zhí)行離開打開和使用資源的范圍時,try-with-resources構(gòu)造會自動關(guān)閉打開的資源,無論是否從該范圍拋出異常。這個構(gòu)造的語法如下:
try (resource acquisitions)
{
// resource usage
}
try關(guān)鍵字由分號分隔的資源獲取語句列表參數(shù)化,其中每條語句獲取一個資源。每個獲取的資源都可用于try塊的主體,并在執(zhí)行離開該主體時自動關(guān)閉。與常規(guī)的try語句不同,try-with-resource不需要catch塊和/或finally塊來跟隨try(),盡管它們可以指定。
考慮以下面向文件的示例:
try (FileInputStream fis = new FileInputStream("abc.txt"))
{
// Do something with fis and the underlying file resource.
}
在這個例子中,獲取了底層文件資源(abc.txt)的輸入流。try塊使用這個資源執(zhí)行某些操作,流(以及文件)在退出try塊時關(guān)閉。
將“var”與“try-with-resource”一起使用
JDK 10引入了對var的支持,var是一種具有特殊含義的標(biāo)識符(即不是關(guān)鍵字)??梢允褂胿ar和try with資源來減少樣板。例如,可以將前面的示例簡化為以下內(nèi)容:
try (var fis = new FileInputStream("abc.txt"))
{
// Do something with fis and the underlying file resource.
}
在try-with-resource場景中復(fù)制文件
本文作者從文件復(fù)制應(yīng)用程序中摘錄了copy()方法。這種方法的finally塊包含前面介紹的文件關(guān)閉樣板。清單8通過使用try-with-resources處理清理,從而改進(jìn)這種方法。
清單8. Copy.java
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Copy
{
public static void main(String[] args)
{
if (args.length != 2)
{
System.err.println("usage: java Copy srcfile dstfile");
return;
}
try
{
copy(args[0], args[1]);
}
catch (IOException ioe)
{
System.err.println("I/O error: " + ioe.getMessage());
}
}
static void copy(String srcFile, String dstFile) throws IOException
{
try (FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(dstFile))
{
int c;
while ((c = fis.read()) != -1)
fos.write(c);
}
}
}
copy()使用try-with-resources來管理源文件和目標(biāo)文件資源。下面的圓括號代碼嘗試創(chuàng)建這些文件的文件輸入和輸出流。假設(shè)成功,它的主體將執(zhí)行,將源文件復(fù)制到目標(biāo)文件。
無論是否拋出異常,try-with-resources都能確保在執(zhí)行離開try塊時關(guān)閉這兩個文件。因為不需要前面顯示的樣板文件關(guān)閉代碼,所以清單8的copy()方法要簡單得多,也更容易閱讀。
設(shè)計資源類以支持try-with-resources
try-with-resources構(gòu)造要求資源類實現(xiàn)java.lang.Closeable接口或JDK 7引入的java.lang.AutoCloseable超級接口。Java7之前的類(如Java.io.FileInputStream)實現(xiàn)了Closeable接口,它提供了一個拋出IOException或子類的void close()方法。
從Java 7開始,類可以實現(xiàn)AutoCloseable,其單個void close()方法可以拋出Java.lang.Exception或子類。throws子句已經(jīng)擴(kuò)展,以適應(yīng)可能需要添加close()方法的情況,這些方法可以在IOException層次結(jié)構(gòu)之外拋出異常;例如java.sql.SQLException。
清單9顯示了一個CustomARM應(yīng)用程序,它展示了如何配置自定義資源類,以便可以在try-with-resources場景中使用它。
清單9. CustomARM.java
public class CustomARM
{
public static void main(String[] args)
{
try (USBPort usbp = new USBPort())
{
System.out.println(usbp.getID());
}
catch (USBException usbe)
{
System.err.println(usbe.getMessage());
}
}
}
class USBPort implements AutoCloseable
{
USBPort() throws USBException
{
if (Math.random() < 0.5)
throw new USBException("unable to open port");
System.out.println("port open");
}
@Override
public void close()
{
System.out.println("port close");
}
String getID()
{
return "some ID";
}
}
class USBException extends Exception
{
USBException(String msg)
{
super(msg);
}
}
清單9模擬了一個USB端口,可以打開和關(guān)閉該端口,并返回端口的 ID。在構(gòu)造函數(shù)中,使用了 Math.random() 來模擬可能拋出異常的情況,以便可以觀察到 try-with-resources 語句在異常被拋出或未拋出時的行為。
編譯這個清單并運行應(yīng)用程序。如果端口打開,將看到以下輸出:
port open
some ID
port close
如果端口關(guān)閉,將看到以下輸出:
unable to open port
在try-with-resources中抑制異常
如果你有一定的編程經(jīng)驗,可能會注意到try-with-resources結(jié)構(gòu)中存在一個潛在的問題:假設(shè)try塊拋出了一個異常。這個結(jié)構(gòu)通過調(diào)用資源對象的close()方法來關(guān)閉資源以做出響應(yīng)。然而,close()方法本身也可能拋出異常。
當(dāng)close()方法拋出異常時(例如,F(xiàn)ileInputStream的void close()方法可以拋出IOException),這個異常會掩蓋或隱藏原始異常。這看起來像是原始的異常丟失了。
實際上,情況并非如此:try-with-resources會抑制close()拋出的異常。它還通過調(diào)用java.lang.Throwable的void addSuppressed(Throwable exception)方法,將異常添加到原始異常的抑制異常數(shù)組中。
清單10展示了一個SupExDemo應(yīng)用程序,它演示了如何在try-with-resources的場景中抑制異常。
清單10. SupExDemo.java
import java.io.Closeable;
import java.io.IOException;
public class SupExDemo implements Closeable
{
@Override
public void close() throws IOException
{
System.out.println("close() invoked");
throw new IOException("I/O error in close()");
}
public void doWork() throws IOException
{
System.out.println("doWork() invoked");
throw new IOException("I/O error in work()");
}
public static void main(String[] args) throws IOException
{
try (SupExDemo supexDemo = new SupExDemo())
{
supexDemo.doWork();
}
catch (IOException ioe)
{
ioe.printStackTrace();
System.out.println();
System.out.println(ioe.getSuppressed()[0]);
}
}
}
清單10的doWork()方法拋出IOException來模擬某種I/O錯誤。close()方法還會拋出IOException,但這個異常被抑制,這樣它就不會掩蓋doWork()的異常。
catch塊通過調(diào)用Throwable的Throwable[]getSuppressed()方法來訪問被抑制的異常(從close()拋出的異常),該方法返回一個包含被抑制異常的數(shù)組。由于在這個例子中只有一個異常被抑制,所以只訪問了數(shù)組的第一個元素。
編譯清單10并運行應(yīng)用程序。應(yīng)該觀察以下輸出:
doWork() invoked
close() invoked
java.io.IOException: I/O error in work()
at SupExDemo.doWork(SupExDemo.java:16)
at SupExDemo.main(SupExDemo.java:23)
Suppressed: java.io.IOException: I/O error in close()
at SupExDemo.close(SupExDemo.java:10)
at SupExDemo.main(SupExDemo.java:24)
java.io.IOException: I/O error in close()
多重捕獲塊(multi-catch)
從JDK 7開始,可以在單個catch塊中捕獲多種類型的異常。這種多重捕獲特性的目的是減少代碼重復(fù),并減少捕獲過于寬泛的異常(例如,catch (Exception e))的誘惑。
假設(shè)開發(fā)了一個應(yīng)用程序,可以靈活地將數(shù)據(jù)復(fù)制到數(shù)據(jù)庫或文件中。清單11展示了一個CopyToDatabaseOrFile類,該類模擬了這種情況,并演示了catch塊代碼重復(fù)的問題。
清單11.CopyToDatabaseOrFile.java
import java.io.IOException;
import java.sql.SQLException;
public class CopyToDatabaseOrFile
{
public static void main(String[] args)
{
try
{
copy();
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
// additional handler code
}
catch (SQLException sqle)
{
System.out.println(sqle.getMessage());
// additional handler code that's identical to the previous handler's
// code
}
}
static void copy() throws IOException, SQLException
{
if (Math.random() < 0.5)
throw new IOException("cannot copy to file");
else
throw new SQLException("cannot copy to database");
}
}
JDK 7通過允許在catch塊中指定多個異常類型來克服代碼重復(fù)問題,其中每個連續(xù)的類型都通過在這些類型之間放置豎線(|)與其前一個類型分開:
try
{
copy();
}
catch (IOException | SQLException iosqle)
{
System.out.println(iosqle.getMessage());
}
現(xiàn)在,當(dāng)copy()拋出異常時,異常將被捕獲并由catch塊處理。
當(dāng)在catch塊的頭文件中列出多個異常類型時,該參數(shù)被隱式地視為final。因此,不能更改參數(shù)的值。例如,不能更改存儲在前一個代碼片段的iosqle參數(shù)中的引用。
縮減字節(jié)碼
編譯處理多種異常類型的catch塊所產(chǎn)生的字節(jié)碼將比編譯每個只處理列出的一種異常類型的幾個catch塊要小。處理多種異常類型的catch塊在編譯期間不會產(chǎn)生重復(fù)的字節(jié)碼。換句話說,字節(jié)碼不包含復(fù)制的異常處理程序。
最終重新拋出異常
從JDK 7開始,Java編譯器能夠比之前的Java版本更精確地分析被重拋的異常。這個特性僅在沒有對被重拋的異常的catch塊參數(shù)進(jìn)行賦值時有效,該參數(shù)被認(rèn)為是有效的final。當(dāng)前面的try塊拋出一個屬于參數(shù)類型的超類型/子類型的異常時,編譯器會拋出捕獲的異常的實際類型,而不是拋出參數(shù)的類型(就像以前的Java版本那樣)。
這個最終重拋異常特性的目的是為了方便在代碼塊周圍添加try-catch語句來攔截、處理并重拋異常,同時不影響從代碼中靜態(tài)確定的異常集。此外,這個特性允許在異常被拋出的地方附近提供一個通用的異常處理器來部分處理異常,并在其他地方提供更精確的處理程序來處理重拋的異常。考慮清單12。
清單12.MonitorEngine.java
class PressureException extends Exception
{
PressureException(String msg)
{
super(msg);
}
}
class TemperatureException extends Exception
{
TemperatureException(String msg)
{
super(msg);
}
}
public class MonitorEngine
{
public static void main(String[] args)
{
try
{
monitor();
}
catch (Exception e)
{
if (e instanceof PressureException)
System.out.println("correcting pressure problem");
else
System.out.println("correcting temperature problem");
}
}
static void monitor() throws Exception
{
try
{
if (Math.random() < 0.1)
throw new PressureException("pressure too high");
else
if (Math.random() > 0.9)
throw new TemperatureException("temperature too high");
else
System.out.println("all is well");
}
catch (Exception e)
{
System.out.println(e.getMessage());
throw e;
}
}
}
清單12模擬了一個實驗性火箭發(fā)動機(jī)的測試,以檢查發(fā)動機(jī)的壓力或溫度是否超過了安全閾值。它通過monitor()助手方法執(zhí)行這個測試。
monitor()方法的try塊在檢測到極端壓力時拋出PressureException,在檢測到極端溫度時拋出TemperatureException。(由于這只是一個模擬,因此使用了隨機(jī)數(shù)——java.lang.Math類的靜態(tài)double random()方法返回一個介于0.0和(接近)1.0之間的隨機(jī)數(shù)。)try塊后面跟著一個catch塊,這個catch塊的目的是通過輸出警告消息來部分處理異常。然后重新拋出此異常,以便monitor()的調(diào)用方法可以完成對異常的處理。
在JDK 7之前,不能在monitor()的throws子句中指定PressureException和TemperatureException,因為catch塊的e參數(shù)是java.lang.Exception類型,并且重新拋出異常被視為拋出參數(shù)的類型。JDK 7及后續(xù)JDK允許在throws子句中指定這些異常類型,因為它們的編譯器可以確定通過throw e拋出的異常來自try塊,并且只能從該塊拋出PressureException和TemperatureException。
因為現(xiàn)在可以指定靜態(tài)void monitor()拋出PressureException, TemperatureException,可以在調(diào)用monitor()時提供更精確的處理程序,如下面的代碼片段所示:
try
{
monitor();
}
catch (PressureException pe)
{
System.out.println("correcting pressure problem");
}
catch (TemperatureException te)
{
System.out.println("correcting temperature problem");
}
由于JDK 7中的最終重新拋出提供了改進(jìn)的類型檢查,因此在以前版本的Java下編譯的源代碼可能無法在以后的JDK下編譯。例如,考慮清單13。
清單13.BreakageDemo.java
class SuperException extends Exception
{
}
class SubException1 extends SuperException
{
}
class SubException2 extends SuperException
{
}
public class BreakageDemo
{
public static void main(String[] args) throws SuperException
{
try
{
throw new SubException1();
}
catch (SuperException se)
{
try
{
throw se;
}
catch (SubException2 se2)
{
}
}
}
}
清單13可以在JDK 6和更早版本下編譯。但是,它無法在后續(xù)版本的 JDK中編譯,因為這些JDK的編譯器會檢測并報告這樣一個事實,即在相應(yīng)的try語句體中從未拋出subeexception2。這是一個小問題,可能很少會在你的程序中遇到,讓編譯器檢測冗余代碼源是值得的。移除這些冗余代碼可以使代碼更加清晰,并且生成的類文件更小。
StackWalker和StackWalking API
通過Thread或Throwable的getStackTrace()方法獲取堆棧跟蹤代價高昂,并且會影響性能。JVM急切地捕獲整個堆棧的快照(隱藏的堆棧幀除外),即使只需要前幾個幀。此外,其代碼可能必須處理不感興趣的幀,這也很耗時。最后,無法訪問由堆棧幀表示的方法所聲明的類的實際 java.lang.Class 實例。為訪問這個Class對象,必須擴(kuò)展java.lang.SecurityManager以訪問受保護(hù)的getClassContext()方法,該方法返回當(dāng)前執(zhí)行堆棧作為 Class 對象的數(shù)組。
JDK 9引入了java.lang.StackWalker類(及其嵌套的Option類和StackFrame接口),作為StackTraceElement(加上SecurityManager)的一個性能更高、功能更強(qiáng)的替代方案。
總結(jié)
本文完成了對Java異常處理幀的兩部分介紹??赡苄枰ㄟ^回顧Java教程中的Oracle異常課程來加強(qiáng)對這個幀的理解。另一個很好的資源是Baeldung的Java異常處理教程,其中包括異常處理中的反模式。
原文標(biāo)題:Exceptions in Java: Advanced features and types,作者:Jeff Friesen