corejava11(7.2 捕获异常)

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子句中指定的类的异常,则

  1. 程序跳过try块中的其余代码。
  2. 程序执行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子句的三种可能情况。

  1. 代码没有抛出异常。在这种情况下,程序首先执行try块中的所有代码。然后,它执行finally子句中的代码。之后,执行将继续执行finally子句之后的第一条语句。换句话说,执行通过点1、2、5和6。
  2. 在我们的例子中,代码抛出了一个catch子句中捕获的异常,即IOException。为此,程序执行try块中的所有代码,直到抛出异常为止。将跳过try块中的其余代码。然后,程序执行匹配catch子句中的代码,然后执行finally子句中的代码。
    如果catch子句没有抛出异常,那么程序将执行finally子句之后的第一行。在这个场景中,执行通过点1、3、4、5和6。
    如果catch子句抛出一个异常,那么该异常将被抛出回此方法的调用方,并且执行仅通过点1、3和5。
  3. 代码引发了一个未在任何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子句中放入更改控制流(returnthrowbreakcontinue)的语句。

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.4
  • Throwable(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语句中,其中tclose方法引发的异常。
  • Throwable[] getSuppressed() 7
    获取此异常的所有“抑制”异常。通常,这些异常是由try-with-resources语句中的close方法引发的。

java.lang.Exception 1.0

  • Exception(Throwable cause) 1.4
  • Exception(String message, Throwable cause)
    构造具有给定原因的Exception

java.lang.RuntimeException 1.0

  • RuntimeException(Throwable cause) 1.4
  • RuntimeException(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_REFERENCESHOW_HIDDEN_FRAMESSHOW_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()
    返回一个格式化字符串,其中包含类和方法名、文件名和行号(如果可用)。

猜你喜欢

转载自blog.csdn.net/nbda1121440/article/details/91496560