7.2 捕获异常
你现在知道如何抛出异常了。这很容易:你扔了就忘了。当然,有些代码必须捕获异常。捕获异常需要更多的计划。这就是下一节将要讨论的内容。
7.2.1 捕获一个异常
如果在任何地方都没有捕捉到异常,程序将终止并向控制台打印一条消息,给出异常的类型和堆栈跟踪。GUI程序(applets和应用程序)捕获异常,打印堆栈跟踪消息,然后返回用户界面处理循环。(调试GUI程序时,最好将控制台保持在屏幕上,而不是最小化。)
要捕获异常,请设置一个try/catch
块。try
块的最简单形式如下:
try
{
code
more code
more code
}
catch (ExceptionType e)
{
handler for this type
}
如果try
块中的任何代码引发catch
子句中指定的类的异常,则
- 程序跳过
try
块中的其余代码。 - 程序执行
catch
子句中的处理程序代码。
如果try
块中没有任何代码引发异常,那么程序将跳过catch
子句。
如果方法中的任何代码引发catch
子句中指定类型以外的异常,则此方法立即退出。(希望它的一个调用方已经为该类型提供了catch
子句。)
为了在工作中展示这一点,下面是一些非常典型的数据读取代码:
public void read(String filename)
{
try
{
var in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1)
{
process input
}
}
catch (IOException exception)
{
exception.printStackTrace();
}
}
请注意,try
子句中的大多数代码都很简单:它读取和处理字节,直到遇到文件结尾。从Java API中可以看出,read
方法可能会抛出一个IOException
。在这种情况下,我们跳过整个while
循环,输入catch
子句,并生成堆栈跟踪。对于一个玩具程序来说,这似乎是一个合理的方法来处理这个例外。你还有别的选择吗?
通常,最好的选择是什么都不做,只将异常传递给调用方。如果read
方法中出现错误,让read
方法的调用方担心它!如果我们采用这种方法,那么我们必须公布这样一个事实,即该方法可能会抛出IOException
。
public void read(String filename) throws IOException
{
var in = new FileInputStream(filename);
int b;
while ((b = in.read()) != -1)
{
process input
}
}
记住,编译器严格地强制执行throws
说明符。如果调用一个方法来抛出一个检查异常,则必须处理它或传递它。
哪一个更好?作为一般规则,您应该捕获那些您知道如何处理的异常,并传播那些您不知道如何处理的异常。
传播异常时,必须添加throws
说明符以提醒调用方可能会引发异常。
查看Java API文档,看看哪些方法抛出哪些异常。然后决定是处理它们还是将它们添加到throws
列表中。后一种选择没有什么令人尴尬的。最好是将异常定向到一个有能力的处理程序,而不是压制它。
请记住,正如我们前面提到的,这个规则有一个例外。如果编写的方法重写了不引发异常的超类方法(如JComponent
中的paintComponent
),则必须在方法的代码中捕获每个选中的异常。不允许向子类方法添加比超类方法中多的throws
说明符。
C++注意
在Java和C++中捕获异常几乎是相同的。严格来说,类比
catch (Exception e) // Java
是
catch (Exception& e) // C++
没有对C++中catch(…)的类比。Java中不需要这样做,因为所有异常都来自一个公共的超类。
7.2.2 捕获多个异常
您可以在一个try
块中捕获多个异常类型,并以不同的方式处理每个类型。对每种类型使用单独的catch
子句,如下例所示:
try
{
code that might throw exceptions
}
catch (FileNotFoundException e)
{
emergency action for missing files
}
catch (UnknownHostException e)
{
emergency action for unknown hosts
}
catch (IOException e)
{
emergency action for all other I/O problems
}
异常对象可能包含有关异常性质的信息。要了解有关该对象的更多信息,请尝试
e.getMessage()
获取详细的错误消息(如果有),或
e.getClass().getName()
获取异常对象的实际类型。
至于Java 7,您可以在同一catch
子句中捕获多个异常类型。例如,假设丢失文件和未知主机的操作相同。然后您可以组合catch
子句:
try
{
code that might throw exceptions
}
catch (FileNotFoundException | UnknownHostException e)
{
emergency action for missing files and unknown hosts
}
catch (IOException e)
{
emergency action for all other I/O problems
}
仅当捕获彼此不属于子类的异常类型时才需要此功能。
注意
当捕获多个异常时,异常变量隐式为final。例如,不能在子句正文中为
e
指定不同的值。catch (FileNotFoundException | UnknownHostException e) { . . . }
注意
捕获多个异常不仅使代码看起来更简单,而且效率更高。生成的字节码包含共享
catch
子句的单个块。
7.2.3 重发和链接异常
您可以在catch
子句中引发异常。通常,当您想更改异常类型时,可以这样做。如果您构建了一个其他程序员使用的子系统,那么使用一个表示子系统故障的异常类型是很有意义的。这种异常类型的一个例子是ServletException
。执行servlet的代码可能不想详细地知道出了什么问题,但它肯定想知道servlet有问题。
以下是捕获异常并重新引发异常的方法:
try
{
access the database
}
catch (SQLException e)
{
throw new ServletException("database error: " + e.getMessage());
}
在这里,ServletException
是用异常的消息文本构造的。
但是,最好将原始异常设置为新异常的“原因”:
try
{
access the database
}
catch (SQLException original)
{
var e = new ServletException("database error");
e.initCause(original);
throw e;
}
捕获异常时,可以检索原始异常:
Throwable original = caughtException.getCause();
强烈推荐使用这种包装技术。它允许您在子系统中抛出高级异常,而不会丢失原始故障的细节。
提示
如果在不允许抛出检查异常的方法中发生检查异常,包装技术也很有用。您可以捕获选中的异常并将其包装为运行时异常。
有时,您只想记录一个异常,并在不做任何更改的情况下重新执行它:
try
{
access the database
}
catch (Exception e)
{
logger.log(level, message, e);
throw e;
}
在Java 7之前,这种方法存在问题。假设代码在一个方法中
public void updateRecord() throws SQLException
Java编译器查看catch
块内的throw
语句,然后查看e
的类型,并抱怨该方法可能抛出任何Exception
,而不只是SQLException
。现在情况有所改善。编译器现在跟踪e
源于try
块的事实。如果该块中唯一选中的异常是SqlException
实例,并且在catch
块中不更改e
,则将封闭方法声明为throws SQLException
是有效的。
7.2.4 fianlly
子句
当代码抛出异常时,它将停止处理方法中剩余的代码并退出该方法。如果方法获得了一些本地资源(只有此方法知道),并且必须清除该资源,则这是一个问题。一种解决方案是捕获所有异常,执行清理,然后重新引发异常。但是这个解决方案很冗长,因为您需要在正常代码和异常代码中的两个位置清理资源分配。finally
子句可以解决这个问题。
注意
自Java 7以来,有一个更优雅的解决方案,即在下面的部分中看到的
try-with-resources
语句。因为它是概念基础,所以我们详细讨论了finally
机制。但在实践中,您可能会比finally
子句更频繁地使用try-with-resource
语句。
无论是否捕获异常,finally
子句中的代码都将执行。在下面的示例中,程序将在所有情况下关闭输入流:
var in = new FileInputStream(. . .);
try
{
// 1
code that might throw exceptions
// 2
}
catch (IOException e)
{
// 3
show error message
// 4
}
finally
{
// 5
in.close();
}
// 6
让我们看看程序将执行finally
子句的三种可能情况。
- 代码没有抛出异常。在这种情况下,程序首先执行
try
块中的所有代码。然后,它执行finally
子句中的代码。之后,执行将继续执行finally
子句之后的第一条语句。换句话说,执行通过点1、2、5和6。 - 在我们的例子中,代码抛出了一个
catch
子句中捕获的异常,即IOException
。为此,程序执行try
块中的所有代码,直到抛出异常为止。将跳过try
块中的其余代码。然后,程序执行匹配catch
子句中的代码,然后执行finally
子句中的代码。
如果catch
子句没有抛出异常,那么程序将执行finally
子句之后的第一行。在这个场景中,执行通过点1、3、4、5和6。
如果catch
子句抛出一个异常,那么该异常将被抛出回此方法的调用方,并且执行仅通过点1、3和5。 - 代码引发了一个未在任何
catch
子句中捕获的异常。在这里,程序执行try
块中的所有代码,直到抛出异常为止。将跳过try
块中的其余代码。然后,执行finally
子句中的代码,并将异常返回给该方法的调用方。执行仅通过点1和5。
您可以使用finally
子句而不使用catch
子句。例如,考虑下面的try
语句:
InputStream in = . . .;
try
{
code that might throw exceptions
}
finally
{
in.close();
}
无论在try
块中是否遇到异常,finally
子句中的in.close()
语句都将被执行。当然,如果遇到异常,它将被重新引发,并且必须在另一个catch
子句中捕获。
InputStream in = . . .;
try
{
try
{
code that might throw exceptions
}
finally
{
in.close();
}
}
catch (IOException e)
{
show error message
}
内部try
块只有一个职责:确保输入流已关闭。外部try
块只有一个职责:确保报告错误。这个解决方案不仅更清晰,而且更实用:报告finally
子句中的错误。
小心
finally
子句包含return
语句时可能会产生意外的结果。假设使用return
语句退出try
块的中间。在方法返回之前,将执行finally
块。如果finally
块还包含一个return
语句,那么它将屏蔽原始返回值。考虑这个例子:public static int parseInt(String s) { try { return Integer.parseInt(s); } finally { return 0; // ERROR } }
在调用
parseInt("42")
中,try
块的主体似乎返回整数42。但是,finally
子句在方法实际返回之前执行,并导致方法返回0,忽略原始返回值。更糟的是。考虑调用
parseInt("zero")
。Integer.parseInt
方法引发了一个NumberFormatException
。然后执行finally
子句,return
语句将吞下异常!
finally
子句的主体用于清理资源。不要在finally
子句中放入更改控制流(return
、throw
、break
、continue
)的语句。
7.2.5 try-with-resource
语句
对于Java 7,对于代码模式有一个有用的快捷方式。
open a resource
try
{
work with the resource
}
finally
{
close the resource
}
如果资源属于实现AutoClosable
接口的类。那个接口只有一个方法
void close() throws Exception
注意
还有一个
Closeable
接口。它是一个AutoCloseable
子接口,也有一个close
方法。但是,该方法声明为引发IOException
。
在其最简单的变体中,try-with-resources
语句的形式为
try (Resource res = . . .)
{
work with res
}
当try
块退出时,将自动调用res.close()
。下面是一个读取文件中所有单词的典型示例:
try (var in = new Scanner(
new FileInputStream("/usr/share/dict/words"), StandardCharsets.UTF_8))
{
while (in.hasNext())
System.out.println(in.next());
}
当块正常退出或出现异常时,调用in.close()
方法,就像使用finally
块一样。
可以指定多个资源。例如,
try (var in = new Scanner(
new FileInputStream("/usr/share/dict/words"), StandardCharsets
var out = new PrintWriter("out.txt", StandardCharsets.UTF_8))
{
while (in.hasNext())
out.println(in.next().toUpperCase());
}
不管这个代码块怎么走,进出都是封闭的。如果手工编程,则需要两个嵌套的try/finally语句。
对于Java 9,可以在try
头部提供先前声明的有效的final变量:
public static void printAll(String[] lines, PrintWriter out)
{
try (out) { // effectively final variable
for (String line : lines)
out.println(line);
} // out.close() called here
}
当try
块抛出异常,close
方法也抛出异常时,会出现一个困难。try-with-resources
语句处理这种情况非常优雅。原始异常被重新引发,close
方法引发的任何异常都被视为“已抑制”。它们将自动捕获并使用addSuppressed
方法添加到原始异常中。如果您对它们感兴趣,请调用getSuppressed
方法,该方法从close
方法生成一个被抑制表达式数组。
你不想手工编程。每当需要关闭资源时,请使用try-with-resources
语句。
注意
try-with-resources
语句本身可以有catch
子句甚至finally
子句。这些是在关闭资源之后执行的。
7.2.6 分析堆栈跟踪元素
堆栈跟踪是在程序执行的特定点上所有挂起的方法调用的列表。您几乎可以肯定地看到堆栈跟踪列表,每当Java程序终止时,它们会显示为一个未被清除的异常。
通过调用Throwable
类的printStackTrace
方法,可以访问堆栈跟踪的文本描述。
var t = new Throwable();
var out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
更灵活的方法是StackWalker
类,它生成StackWalker.StackFrame
实例流,每个实例描述一个堆栈帧。您可以使用此调用迭代堆栈帧:
StackWalker walker = StackWalker.getInstance();
walker.forEach(frame -> analyze frame)
如果要惰性地处理Stream<StackWalker.StackFrame>,调用
walker.walk(stream -> process stream)
第二卷第一章详细描述了流处理。
StackWalker.StackFrame
类具有获取执行代码行的文件名和行号以及类对象和方法名的方法。toString
方法生成一个包含所有这些信息的格式化字符串。
注意
在Java 9之前,
Throwable.getStackTrace
方法产生了一个StackTraceElement[]
数组,它与StasWalk.StackFrame
实例的流具有类似的信息。但是,该调用效率较低,因为它捕获整个堆栈,即使调用方可能只需要几个帧,并且它只提供对挂起方法的类名(而不是类对象)的访问。
清单7.1打印递归阶乘函数的堆栈跟踪。例如,如果计算factorial(3)
,则打印输出为
factorial(3):
stackTrace.StackTraceTest.factorial(StackTraceTest.java:20)
stackTrace.StackTraceTest.main(StackTraceTest.java:36)
factorial(2):
stackTrace.StackTraceTest.factorial(StackTraceTest.java:20)
stackTrace.StackTraceTest.factorial(StackTraceTest.java:26)
stackTrace.StackTraceTest.main(StackTraceTest.java:36)
factorial(1):
stackTrace.StackTraceTest.factorial(StackTraceTest.java:20)
stackTrace.StackTraceTest.factorial(StackTraceTest.java:26)
stackTrace.StackTraceTest.factorial(StackTraceTest.java:26)
stackTrace.StackTraceTest.main(StackTraceTest.java:36)
return 1
return 2
return 6
清单7.1 stackTrace/StackTraceTest.java
package stackTrace;
import java.util.*;
/**
* A program that displays a trace feature of a recursive method call.
* @version 1.10 2017-12-14
* @author Cay Horstmann
*/
public class StackTraceTest
{
/**
* Computes the factorial of a number
* @param n a non-negative integer
* @return n! = 1 * 2 * . . . * n
*/
public static int factorial(int n)
{
System.out.println("factorial(" + n + "):");
var walker = StackWalker.getInstance();
walker.forEach(System.out::println);
int r;
if (n <= 1) r = 1;
else r = n * factorial(n - 1);
System.out.println("return " + r);
return r;
}
public static void main(String[] args)
{
try (var in = new Scanner(System.in))
{
System.out.print("Enter n: ");
int n = in.nextInt();
factorial(n);
}
}
}
java.lang.Trhowable
1.0
Throwable(Throwable cause)
1.4Throwable(String message, Throwable cause)
1.4
构造具有给定原因的Throwable
。Throwable initCause(Throwable cause)
1.4
设置此对象的原因,如果此对象已有原因,则引发异常。返回this
。Throwable getCause()
1.4
获取设置为此对象的原因的异常对象,如果未设置原因,则为null
。StackTraceElement[] getStackTrace()
1.4
获取构造此对象时调用堆栈的跟踪。void addSuppressed(Throwable t)
7
将“抑制”异常添加到此异常。这发生在try-with-resources
语句中,其中t
是close
方法引发的异常。Throwable[] getSuppressed()
7
获取此异常的所有“抑制”异常。通常,这些异常是由try-with-resources
语句中的close方法引发的。
java.lang.Exception
1.0
Exception(Throwable cause)
1.4Exception(String message, Throwable cause)
构造具有给定原因的Exception
。
java.lang.RuntimeException
1.0
RuntimeException(Throwable cause)
1.4RuntimeException(String message, Throwable cause)
1.4
构造具有给定原因的RuntimeException
。
java.lang.StackWalker
9
static StackWalker getInstance()
static StackWalker getInstance(StackWalker.Option option)
static StackWalker getInstance(Set<StackWalker.Option> options)
获取StackWalker
实例。选项包括StackWalker.Option
枚举中的RETAIN_CLASS_REFERENCE
、SHOW_HIDDEN_FRAMES
和SHOW_REFLECT_FRAMES
。forEach(Consumer<? super StackWalker.StackFrame> action)
walk(Function<? super Stream<StackWalker.StackFrame>,? extends T> function)
将给定函数应用于堆栈帧流并返回函数的结果。
java.lang.StackWalker.StackFrame
9
String getFileName()
获取包含此元素执行点的源文件的名称,如果信息不可用,则为null
。int getLineNumber()
获取包含此元素执行点的源文件的行号,如果信息不可用,则为-1。String getClassName()
获取其方法包含此元素执行点的类的完全限定名。String getDeclaringClass()
获取包含此元素的执行点的方法的类对象。如果堆栈遍历器不是用RETAIN_CLASS_REFERENCE
选项构造的,则引发异常。String getMethodName()
获取包含此元素的执行点的方法的名称。构造函数的名称是<init>
。静态初始值设定项的名称是<clinit>
。不能区分同名的重载方法。boolean isNativeMethod()
如果此元素的执行点位于native方法内,则返回true
。String toString()
返回一个格式化字符串,其中包含类和方法名、文件名和行号(如果可用)。
java.lang.StackTraceElement
1.4
-
String getFileName()
获取包含此元素执行点的源文件的名称,如果信息不可用,则为null
。 -
int getLineNumber()
获取包含此元素执行点的源文件的行号,如果信息不可用,则为-1。 -
String getClassName()
获取包含此元素执行点的类的完全限定名。 -
String getMethodName()
获取包含此元素的执行点的方法的名称。构造函数的名称是<init>
。静态初始值设定项的名称是<clinit>
。不能区分同名的重载方法。 -
boolean isNativeMethod()
如果此元素的执行点位于native方法内,则返回true。
-
String toString()
返回一个格式化字符串,其中包含类和方法名、文件名和行号(如果可用)。