审校 | 重楼
Java平台包含了多种语言特性和库类型,用于处理与预期程序行为不同的异常。本文将介绍Java中异常处理的高级特性,其中包括堆栈跟踪(stack traces)、异常链(exception chaining)、try-with-resources、多重捕获(multi-catch)、最终重新抛出异常(final re-throw),以及堆栈遍历(stack walking)。
Java教程的学习内容
以下将介绍在Java程序中处理异常的高级特性:
- 堆栈跟踪和堆栈跟踪元素
- 原因(Causes)和异常链(exception chaining)
- Try-with-resources
- 多重捕获(multi-catch)
- 最终重新抛出异常(final re-throw)
- StackWalking和StackWalking API
使用堆栈跟踪进行异常处理
每个JVM线程(执行路径)都与创建线程时创建的堆栈相关联。这个数据结构被划分为帧(frame),这些帧是与方法调用相关联的数据结构。因此,每个线程的堆栈通常被称为方法调用堆栈。
每当调用一个方法时,就会创建一个新的帧。每帧存储局部变量、参数变量(保存传递给方法的参数)、返回到调用方法的信息、存储返回值的空间、在调度异常时有用的信息等等。
堆栈跟踪是在线程执行过程中某个时间点的活动堆栈帧的报告。Java的Throwable类(在Java.lang包中)提供了打印堆栈跟踪、填充堆栈跟踪和访问堆栈跟踪元素的方法。
下载本教程中示例的源代码。
打印堆栈跟踪
当throw语句抛出一个throwable时,它首先会在当前执行的方法中查找一个合适的catch块。如果没有找到,它会回溯方法调用堆栈,寻找能够处理该异常的最近的catch块。如果仍然没有找到,JVM将终止执行,并显示一条合适的消息。可以在清单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的示例创建了一个java.io.IOException对象,并将该对象抛出main()方法。因为main()不处理这个throwable,而且main()是顶级方法,因此JVM会终止执行,并显示一条合适的消息。对于这个应用程序,将看到以下消息:
Exception in thread "main" java.io.IOException
at PrintStackTraceDemo.main(PrintStackTraceDemo.java:7)
JVM通过调用Throwable的void printStackTrace()方法输出这条消息,该方法在标准错误流上打印调用throwable对象的堆栈跟踪。输出的第一行显示了调用throwable对象的toString()方法的结果。下一行显示了fillInStackTrace()之前记录的数据(稍后讨论)。
其他的打印堆栈跟踪方法
throwable的重载void printStackTrace(PrintStream ps)和void printStackTrace(printwwriter pw)方法将堆栈跟踪输出到指定的流或写入器。
堆栈跟踪显示了创建throwable的源文件和行号。在本例中,它是在PrintStackTrace.java源文件的第7行创建的。
可以直接调用printStackTrace(),通常是调用catch块。例如,考虑PrintStackTraceDemo应用程序的第二个版本。
清单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()方法,它调用方法a(),而方法a()又调用方法b()。方法b()向JVM抛出一个IOException对象,JVM将展开方法调用堆栈,直到找到main()的catch块,该块可以处理异常。异常是通过对throwable调用printStackTrace()来处理的。这种方法产生以下输出:
java.io.IOException
at PrintStackTraceDemo.b(PrintStackTraceDemo.java:24)
at PrintStackTraceDemo.a(PrintStackTraceDemo.java:19)
at PrintStackTraceDemo.main(PrintStackTraceDemo.java:9)
printStackTrace()不会输出线程的名称。与其相反,它首先会在第一行调用 Throwable 对象的 toString() 方法,以返回异常的完全限定类名(如 java.io.IOException),这是输出的第一部分。然后输出方法调用层次结构:最近调用的方法(b())位于顶部,main()位于底部。
堆栈跟踪标识的是哪一行?
堆栈跟踪标识了创建throwable的行,但它不会标识抛出throwable的行(通过throw),除非抛出和创建是在同一行代码中完成的。
填充堆栈跟踪
throwable声明了一个Throwable fillInStackTrace()方法来填充执行堆栈跟踪。在调用Throwable对象中,它记录有关当前线程堆栈帧的当前状态的信息。考虑清单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的主要区别在于catch块的throw (IOException) ioe.fillInStackTrace();声明。该语句替换ioe的堆栈跟踪,然后重新抛出throwable。应该看到这样的输出:
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()的位置,而不是重复标识IOException对象创建位置的初始堆栈跟踪。
Throwable构造函数和fillInStackTrace()
throwable的每个构造函数都调用fillInStackTrace()。然而,当将false传递给writableStackTrace时,下面的构造函数(在JDK 7中引入)不会调用这个方法:
Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
fillInStackTrace()调用一个原生方法,该方法沿着当前线程的方法调用堆栈遍历以构建堆栈跟踪。这种遍历代价高昂,如果过于频繁,可能会影响性能。
如果遇到性能非常关键的情况(可能涉及嵌入式设备),可以通过重写fillInStackTrace()来阻止堆栈跟踪的构建。请查看清单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——对调用Throwable的引用。该程序生成以下输出:
NoStackTraceException
注释掉覆盖的fillInStackTrace()方法,会看到以下输出:
NoStackTraceException
at FillInStackTraceDemo.b(FillInStackTraceDemo.java:22)
at FillInStackTraceDemo.a(FillInStackTraceDemo.java:17)
at FillInStackTraceDemo.main(FillInStackTraceDemo.java:7)
访问堆栈跟踪的元素
有时,需要访问堆栈跟踪的元素,以便提取日志记录、识别资源泄漏源和其他目的所需的详细信息。printStackTrace()和fillInStackTrace()方法不支持这个任务,但是java.lang.StackTraceElement和它的方法就是为这个任务设计的。
stacktraceelement类描述了在堆栈跟踪中表示堆栈帧的元素。它的方法可用于返回类的完全限定名,该类包含这个堆栈跟踪元素所表示的执行点以及其他有用信息。以下是该类的主要方法:
- String getClassName()返回包含这个堆栈跟踪元素所表示的执行点的类的完全限定名。
- String getFileName()返回包含这个堆栈跟踪元素所表示的执行点的源文件的名称。
- int getLineNumber()返回包含这个堆栈跟踪元素所表示的执行点的源行的行号。
- String getMethodName()返回包含这个堆栈跟踪元素所表示的执行点的方法的名称。
- 当包含这个堆栈跟踪元素所表示的执行点的方法是原生方法时,boolean isNativeMethod() 返回true。
另一个重要方法是java.lang.Thread和Throwable类上的StackTraceElement[]getStackTrace()。这个方法分别返回一个堆栈跟踪元素数组,表示调用线程的堆栈转储,并提供对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();
}
}
当运行这个应用程序时,会看到以下输出:
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()方法。这种方法是为远程过程调用(RPC)框架和其他高级系统设计的,允许客户端覆盖在构造Throwable时由fillInStackTrace()生成的默认堆栈跟踪。
之前展示了如何重写fillInStackTrace()以防止构建堆栈跟踪。然而,可以使用StackTraceElement和setStackTrace()来安装新的堆栈跟踪。可以创建一个通过以下构造函数初始化的StackTraceElement对象数组,并将该数组传递给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类名,以证明这个堆栈跟踪是输出的跟踪。运行应用程序,将观察到以下堆栈跟踪:
NoStackTraceException
at *StackTraceElementDemo*.b()(StackTraceElementDemo.java:22)
at *StackTraceElementDemo*.a()(StackTraceElementDemo.java:17)
at *StackTraceElementDemo*.main()(StackTraceElementDemo.java:7)
原因和异常链
在异常处理中,一个catch块经常会通过抛出另一个异常来响应捕获到的异常。第一个异常被认为是导致第二个异常发生的原因,这两个异常被隐式地链接在一起。
例如,调用库方法的catch块时可能会出现内部异常,而该内部异常对标准库方法的调用者不应该是可见的。因为需要通知调用者出现了错误,所以catch块会创建一个符合库方法合约接口的外部异常,并且调用者可以处理这个异常。
由于内部异常可能对于诊断问题非常有帮助,catch块应该将内部异常包装在外部异常中。被包装的异常被称为“原因”(cause),因为它的存在导致抛出外部异常。此外,包装的内部异常(原因)被显式地链接到外部异常。
对原因和异常链的支持是通过两个Throwable构造函数(以及对应的exception、RuntimeException和Error)和两种方法提供的:
- Throwable(String message, Throwable cause)
- Throwable(Throwable cause)
- Throwable getCause()
- Throwable initCause(Throwable cause)
Throwable构造函数(及其对应的子类)允许在构造Throwable时包装原因。如果处理的教学法遗留代码的自定义异常类不支持任何一个构造函数,可以通过在Throwable上调用initCause()来包装原因。需要注意的是,initCause()只能被调用一次。无论哪种方式,都可以通过调用getCause()返回原因。当没有原因时,这个方法返回null。
清单7 展示了一个名为 CauseDemo 的应用程序,该应用程序演示了异常原因(以及异常链)的概念。
清单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()方法调用Library的externalOp()方法并catch其抛出的ExternalException对象。catch块调用printStackTrace()来输出外部异常及其原因。
库的externalOp()方法故意抛出一个InternalException对象,其catch块将该对象映射到一个ExternalException对象。因为ExternalException不支持可以接受cause参数的构造函数,所以使用initCause()来包装InternalException对象。
运行这个应用程序,就会看到下面的堆栈跟踪:
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行对该方法的调用中抛出。第二个堆栈跟踪显示了内部异常原因源自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();.
关于“...more””以及探究原因链的更多信息
通常,“...n more”这样的表述意味着原因的堆栈跟踪的最后n行与前一个堆栈跟踪的最后n行是重复的。
这个例子只揭示了一个原因。然而,从非简单的现实世界应用程序中抛出的异常可能包含由多个原因构成的复杂链。可以通过使用如下所示的循环来访问这些原因:
catch (Exception exc)
{
Throwable t = exc.getCause();
while (t != null)
{
System.out.println(t);
t = t.getCause();
}
}
Try-with-resources
Java应用程序经常访问文件、数据库连接、套接字和其他依赖于相关系统资源的资源(例如文件句柄)。系统资源的稀缺性意味着它们最终必须被释放,即使在发生异常时也是如此。当系统资源没有被释放,应用程序在尝试获取其他资源时最终会失败,因为没有更多相关的系统资源可用。
在对异常处理基础的介绍中,提到资源(实际上是它们所依赖的系统资源)在finally块中释放。这可能会导致冗长的样板代码,例如下面显示的文件关闭代码:
finally
{
if (fis != null)
try
{
fis.close();
}
catch (IOException ioe)
{
// ignore exception
}
if (fos != null)
try
{
fos.close();
}
catch (IOException ioe)
{
// ignore exception
}
}
这个样板代码不仅增加了类文件的容量,而且编写它的单调乏味可能会导致错误,甚至可能无法关闭文件。JDK 7引入了“try-with-resource”来克服这个问题。
try-with-resource的基本原理
当执行离开打开和使用资源的范围时,try-with-resources构造会自动关闭打开的资源,无论是否从该范围抛出异常。这个构造的语法如下:
try (resource acquisitions)
{
// resource usage
}
try关键字由分号分隔的资源获取语句列表参数化,其中每条语句获取一个资源。每个获取的资源都可用于try块的主体,并在执行离开该主体时自动关闭。与常规的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块使用这个资源执行某些操作,流(以及文件)在退出try块时关闭。
将“var”与“try-with-resource”一起使用
JDK 10引入了对var的支持,var是一种具有特殊含义的标识符(即不是关键字)。可以使用var和try with资源来减少样板。例如,可以将前面的示例简化为以下内容:
try (var fis = new FileInputStream("abc.txt"))
{
// Do something with fis and the underlying file resource.
}
在try-with-resource场景中复制文件
本文作者从文件复制应用程序中摘录了copy()方法。这种方法的finally块包含前面介绍的文件关闭样板。清单8通过使用try-with-resources处理清理,从而改进这种方法。
清单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来管理源文件和目标文件资源。下面的圆括号代码尝试创建这些文件的文件输入和输出流。假设成功,它的主体将执行,将源文件复制到目标文件。
无论是否抛出异常,try-with-resources都能确保在执行离开try块时关闭这两个文件。因为不需要前面显示的样板文件关闭代码,所以清单8的copy()方法要简单得多,也更容易阅读。
设计资源类以支持try-with-resources
try-with-resources构造要求资源类实现java.lang.Closeable接口或JDK 7引入的java.lang.AutoCloseable超级接口。Java7之前的类(如Java.io.FileInputStream)实现了Closeable接口,它提供了一个抛出IOException或子类的void close()方法。
从Java 7开始,类可以实现AutoCloseable,其单个void close()方法可以抛出Java.lang.Exception或子类。throws子句已经扩展,以适应可能需要添加close()方法的情况,这些方法可以在IOException层次结构之外抛出异常;例如java.sql.SQLException。
清单9显示了一个CustomARM应用程序,它展示了如何配置自定义资源类,以便可以在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端口,可以打开和关闭该端口,并返回端口的 ID。在构造函数中,使用了 Math.random() 来模拟可能抛出异常的情况,以便可以观察到 try-with-resources 语句在异常被抛出或未抛出时的行为。
编译这个清单并运行应用程序。如果端口打开,将看到以下输出:
port open
some ID
port close
如果端口关闭,将看到以下输出:
unable to open port
在try-with-resources中抑制异常
如果你有一定的编程经验,可能会注意到try-with-resources结构中存在一个潜在的问题:假设try块抛出了一个异常。这个结构通过调用资源对象的close()方法来关闭资源以做出响应。然而,close()方法本身也可能抛出异常。
当close()方法抛出异常时(例如,FileInputStream的void close()方法可以抛出IOException),这个异常会掩盖或隐藏原始异常。这看起来像是原始的异常丢失了。
实际上,情况并非如此:try-with-resources会抑制close()抛出的异常。它还通过调用java.lang.Throwable的void addSuppressed(Throwable exception)方法,将异常添加到原始异常的抑制异常数组中。
清单10展示了一个SupExDemo应用程序,它演示了如何在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块通过调用Throwable的Throwable[]getSuppressed()方法来访问被抑制的异常(从close()抛出的异常),该方法返回一个包含被抑制异常的数组。由于在这个例子中只有一个异常被抑制,所以只访问了数组的第一个元素。
编译清单10并运行应用程序。应该观察以下输出:
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块中捕获多种类型的异常。这种多重捕获特性的目的是减少代码重复,并减少捕获过于宽泛的异常(例如,catch (Exception e))的诱惑。
假设开发了一个应用程序,可以灵活地将数据复制到数据库或文件中。清单11展示了一个CopyToDatabaseOrFile类,该类模拟了这种情况,并演示了catch块代码重复的问题。
清单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块中指定多个异常类型来克服代码重复问题,其中每个连续的类型都通过在这些类型之间放置竖线(|)与其前一个类型分开:
try
{
copy();
}
catch (IOException | SQLException iosqle)
{
System.out.println(iosqle.getMessage());
}
现在,当copy()抛出异常时,异常将被捕获并由catch块处理。
当在catch块的头文件中列出多个异常类型时,该参数被隐式地视为final。因此,不能更改参数的值。例如,不能更改存储在前一个代码片段的iosqle参数中的引用。
缩减字节码
编译处理多种异常类型的catch块所产生的字节码将比编译每个只处理列出的一种异常类型的几个catch块要小。处理多种异常类型的catch块在编译期间不会产生重复的字节码。换句话说,字节码不包含复制的异常处理程序。
最终重新抛出异常
从JDK 7开始,Java编译器能够比之前的Java版本更精确地分析被重抛的异常。这个特性仅在没有对被重抛的异常的catch块参数进行赋值时有效,该参数被认为是有效的final。当前面的try块抛出一个属于参数类型的超类型/子类型的异常时,编译器会抛出捕获的异常的实际类型,而不是抛出参数的类型(就像以前的Java版本那样)。
这个最终重抛异常特性的目的是为了方便在代码块周围添加try-catch语句来拦截、处理并重抛异常,同时不影响从代码中静态确定的异常集。此外,这个特性允许在异常被抛出的地方附近提供一个通用的异常处理器来部分处理异常,并在其他地方提供更精确的处理程序来处理重抛的异常。考虑清单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模拟了一个实验性火箭发动机的测试,以检查发动机的压力或温度是否超过了安全阈值。它通过monitor()助手方法执行这个测试。
monitor()方法的try块在检测到极端压力时抛出PressureException,在检测到极端温度时抛出TemperatureException。(由于这只是一个模拟,因此使用了随机数——java.lang.Math类的静态double random()方法返回一个介于0.0和(接近)1.0之间的随机数。)try块后面跟着一个catch块,这个catch块的目的是通过输出警告消息来部分处理异常。然后重新抛出此异常,以便monitor()的调用方法可以完成对异常的处理。
在JDK 7之前,不能在monitor()的throws子句中指定PressureException和TemperatureException,因为catch块的e参数是java.lang.Exception类型,并且重新抛出异常被视为抛出参数的类型。JDK 7及后续JDK允许在throws子句中指定这些异常类型,因为它们的编译器可以确定通过throw e抛出的异常来自try块,并且只能从该块抛出PressureException和TemperatureException。
因为现在可以指定静态void monitor()抛出PressureException, TemperatureException,可以在调用monitor()时提供更精确的处理程序,如下面的代码片段所示:
try
{
monitor();
}
catch (PressureException pe)
{
System.out.println("correcting pressure problem");
}
catch (TemperatureException te)
{
System.out.println("correcting temperature problem");
}
由于JDK 7中的最终重新抛出提供了改进的类型检查,因此在以前版本的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和更早版本下编译。但是,它无法在后续版本的 JDK中编译,因为这些JDK的编译器会检测并报告这样一个事实,即在相应的try语句体中从未抛出subeexception2。这是一个小问题,可能很少会在你的程序中遇到,让编译器检测冗余代码源是值得的。移除这些冗余代码可以使代码更加清晰,并且生成的类文件更小。
StackWalker和StackWalking API
通过Thread或Throwable的getStackTrace()方法获取堆栈跟踪代价高昂,并且会影响性能。JVM急切地捕获整个堆栈的快照(隐藏的堆栈帧除外),即使只需要前几个帧。此外,其代码可能必须处理不感兴趣的帧,这也很耗时。最后,无法访问由堆栈帧表示的方法所声明的类的实际 java.lang.Class 实例。为访问这个Class对象,必须扩展java.lang.SecurityManager以访问受保护的getClassContext()方法,该方法返回当前执行堆栈作为 Class 对象的数组。
JDK 9引入了java.lang.StackWalker类(及其嵌套的Option类和StackFrame接口),作为StackTraceElement(加上SecurityManager)的一个性能更高、功能更强的替代方案。
总结
本文完成了对Java异常处理帧的两部分介绍。可能需要通过回顾Java教程中的Oracle异常课程来加强对这个帧的理解。另一个很好的资源是Baeldung的Java异常处理教程,其中包括异常处理中的反模式。
原文Exceptions in Java: Advanced features and types,作者:Jeff Friesen