vlambda博客
学习文章列表

JDK17 |java17学习 第 2 部分 Java 的构建块

Chapter 4: Exception Handling

In Chapter 1, Getting Started with Java 17, we briefly introduced exceptions. In this chapter, we will treat this topic more systematically. There are two kinds of exceptions in Java: checked and unchecked. We’ll demonstrate each of them, and the differences between the two will be explained. Additionally, you will learn about the syntax of the Java constructs related to exception handling and the best practices to address (that is, handle) those exceptions. The chapter will end on the related topic of assertion statements, which can be used to debug the code in production.

在本章中,我们将介绍以下主题:

  • Java 异常框架
  • 已检查和未检查(运行时)异常
  • trycatchfinally
  • throws 语句
  • throw 语句
  • assert 语句
  • 异常处理的最佳实践

那么,让我们开始吧!

Technical requirements

为了能够执行本章中提供的代码示例,您将需要以下内容:

  • 装有 Microsoft Windows、Apple macOS 或 Linux 操作系统的计算机
  • Java SE 版本 17 或更高版本
  • 您喜欢的 IDE 或代码编辑器

第 1 章Java 17 入门。本章的代码示例文件可在 GitHub 上的 https:// github.com/PacktPublishing/Learn-Java-17-Programming.git 存储库。请在 examples/src/main/java/com/packt/learnjava/ch04_exceptions 文件夹中搜索。

The Java exceptions framework

第 1 章中所述, Java 17 入门,意外情况可能会导致 Java 虚拟机 (JVM ) 或应用程序代码来创建和抛出异常对象。一旦 发生这种情况, 控制流就会转移到 catch子句,也就是说,如果异常是在 try 块内引发的。让我们看一个例子。考虑以下方法:

void method(String s){
    if(s.equals("abc")){
        System.out.println("Equals abc");
    } else {
        System.out.println("Not equal");
    }
}

如果输入参数值为 null,您可能会看到输出为 Not equal。不幸的是,事实并非如此。 s.equals("abc") 表达式在 equals() 方法="literal">s 变量;但是,如果 s 变量为 null,则它不引用任何对象。让我们看看接下来会发生什么。

让我们运行以下代码(即 Framework 类中的 catchException1() 方法):

try {
    method(null);
} catch (Exception ex){
    System.out.println("catchException1():");
    System.out.println(ex.getClass().getCanonicalName());  
                       //prints: java.lang.NullPointerException
    waitForStackTrace();
    ex.printStackTrace();  //prints: see the screenshot
    if(ex instanceof NullPointerException){
        //do something
    } else {
        //do something else
    }
}

前面的代码包含 waitForStackTrace() 方法,允许您稍等片刻,直到生成堆栈跟踪。否则,输出将乱序。此代码的输出如下所示:

catchException1():                                                  
java.lang.NullPointerException                                      
java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "s" is null                                                 
     at com.packt.learnjava.ch04_exceptions.Framework.method(Framework.java:14)
     at com.packt.learnjava.ch04_exceptions.Framework.catchException1(Framework.java:24)
     at com.packt.learnjava.ch04_exceptions.Framework.main(Framework.java:8)

如您所见,该方法打印出异常类的 名称,然后是 堆栈跟踪。 stack trace 的名称来自方法调用在 JVM 内存中存储(作为堆栈)的方式:一个方法调用另一个方法,而后者又调用另一个方法,以此类推上。最内层方法返回后,往回走栈,将返回的方法(栈帧)从栈中移除.我们将在 第 9 章JVM 结构和垃圾收集。当发生异常时,所有堆栈内容(例如堆栈帧)都作为堆栈跟踪返回。这使我们能够追踪 导致问题的代码行。

在前面的代码示例中,根据异常的类型执行了不同的代码块。在我们的例子中,它是 java.lang.NullPointerException。如果应用程序代码没有捕捉到它,这个异常将通过调用方法的堆栈传播到 JVM,然后它会停止执行应用程序。为避免这种情况发生,可以捕获异常并执行代码以从异常情况中恢复。

Java 中异常处理框架的目的是保护应用程序代码免受意外情况的影响,并在可能的情况下从中恢复。在接下来的部分中,我们将更详细地剖析这个概念,并使用框架功能重写给定的示例。

Checked and unchecked exceptions

如果您查看 java.lang 包 API 的 文档,您会发现该包包含近三打异常类和 几十个错误类。两个组都扩展了java.lang.Throwable类,继承了它的所有方法,并且不添加其他方法。 java.lang.Throwable 类中最常用的方法包括以下

  • void printStackTrace():输出方法调用的堆栈跟踪(堆栈帧)。
  • StackTraceElement[] getStackTrace():这将返回与 printStackTrace() 相同的信息,但允许以编程方式访问堆栈的任何帧痕迹。
  • String getMessage():这会检索通常包含对异常或错误原因的用户友好说明的消息。
  • Throwable getCause():这会检索 java.lang.Throwable 的可选对象,该对象是异常的原始原因(但代码的作者决定将其包装在另一个异常或错误中)。

所有错误都扩展了 java.lang.Error 类,而后者又扩展了 java.lang.Throwable 类。通常,JVM 抛出一个错误,根据官方文档,表示一个合理的应用程序不应该尝试捕获的严重问题 。以下是一些 示例:

  • OutOfMemoryError:当 JVM 内存不足且无法使用垃圾回收清理内存时抛出此错误。
  • StackOverflowError:当为方法调用的堆栈分配的内存不足以存储另一个堆栈帧时,会抛出此错误。
  • NoClassDefFoundError:当JVM找不到当前加载的类所请求的类的定义时抛出该错误。

该框架的作者假设应用程序无法自动从这些错误中恢复,这被证明是一个基本正确的假设。这就是为什么程序员通常不会捕捉错误的原因,但这超出了本书的范围。

另一方面,异常通常与特定于应用程序的问题有关,通常不需要我们关闭应用程序并允许恢复。通常,这就是程序员捕获它们并实现应用程序逻辑的替代(主流)路径的原因,或者至少在不关闭应用程序的情况下报告问题。这里有一些例子:

  • ArrayIndexOutOfBoundsException:当代码试图通过 索引访问等于或大于数组长度(请记住,数组的第一个元素的索引为 0,因此索引等于数组外的数组长度点)。
  • ClassCastException:当代码强制转换对与变量所引用的对象无关的类或接口的引用时,将抛出此异常。
  • NumberFormatException:当代码尝试将字符串转换为数字类型,但字符串不包含必要的数字格式时抛出此异常。

所有异常都扩展了 java.lang.Exception 类,而后者又扩展了 java.lang.Throwable 类。这就是为什么通过捕获 java.lang.Exception 类的对象,代码可以捕获任何异常类型的对象。在 Java 异常框架 部分,我们通过以相同方式捕获 java.lang.NullPointerException 来演示这一点。

java.lang.RuntimeException 是一个例外。扩展它的异常是称为运行时异常或未经检查的异常。我们已经提到了其中的一些NullPointerExceptionArrayIndexOutOfBoundsExceptionClassCastExceptionNumberFormatException。它们被称为运行时异常的原因很清楚;它们被称为未经检查的异常的原因将在接下来变得清晰。

那些在其祖先中没有 java.lang.RuntimeException 的异常是称为检查异常。使用这种名称的原因是编译器确保(检查)这些异常已被捕获或列在方法的 throws 子句中(请参阅 抛出语句部分)。这种设计迫使程序员做出有意识的决定,要么捕获检查的异常,要么通知客户端该方法可能会抛出该异常并且必须由客户端处理(处理)。以下是一些已检查异常的示例:

  • ClassNotFoundException:当尝试使用 forName() 方法使用其字符串名称加载类时抛出此异常class="literal">Class 类失败。
  • CloneNotSupportedException:当代码试图克隆一个没有实现 Cloneable 接口的对象时抛出这个异常。
  • NoSuchMethodException:当代码没有调用任何方法时抛出。

并非所有异常都驻留在 java.lang 包中。许多其他包包含与包支持的功能相关的异常。例如,有一个 java.util.MissingResourceException 运行时异常和一个 java.io.IOException 已检查异常。

尽管不是被迫的,但程序员经常捕获运行时(未经检查的)异常以更好地控制程序流,从而使应用程序的行为更加稳定和可预测。顺便说一句,所有错误也是运行时(未经检查的)异常。然而,正如我们已经提到的,通常不可能以编程方式处理它们,因此捕获 java.lang.Error 类的后代是没有意义的。

The try, catch, and finally blocks

try 块内抛出异常时,它将控制流重定向到第一个 catch 子句。如果有没有catch块可以捕获异常(但是finally 块有 就位),异常向上传播并离开方法。如果有多个 catch 子句,编译器会强制您排列它们,以便子异常列在父异常之前。让我们看下面的例子:

void someMethod(String s){
    try {
       method(s);
    } catch (NullPointerException ex){
       //do something
    } catch (Exception ex){
       //do something else
    }
}

在前面的示例中,带有 NullPointerExceptioncatch 块被放置在带有 Exception< 的块之前/code> 因为 NullPointerException 扩展了 RuntimeException,而后者又扩展了 Exception。我们甚至可以实现这个例子,如下所示:

void someMethod(String s){
    try {
        method(s);
    } catch (NullPointerException ex){
        //do something
    } catch (RuntimeException ex){
        //do something else
    } catch (Exception ex){
        //do something different
    }
}

请注意,第一个 catch 子句仅捕获 NullPointerException。扩展 RuntimeException 的其他异常将被第二个 catch 子句捕获。其余的异常类型(即所有检查的异常)将被最后一个 catch 块捕获。请注意,这些 catch 子句中的任何一个都不会捕获错误。要捕获它们,您应该为 Error (在任何位置)或 添加一个 catch 子句Throwable(在上一个示例中的最后一个 catch 子句之后)。但是,程序员通常不会这样做,并允许错误传播到 JVM 中。

为每个异常类型设置一个catch块允许我们提供特定的异常类型处理。但是,如果在异常处理方面没有区别,您可以简单地用一个 Exception 基类的 catch 块来捕获所有类型的异常:

void someMethod(String s){
    try {
        method(s);
    } catch (Exception ex){
        //do something
    }
}

如果没有子句捕获异常,则将其进一步向上抛出,直到由方法调用者之一中的 try...catch 语句处理或一直传播出应用程序代码。在这种情况下,JVM 会终止应用程序并退出。

添加 finally 不会改变所描述的行为。如果存在,它总是被执行,无论是否产生了异常。通常,finally 块用于释放资源、关闭数据库连接、文件或类似内容。但是,如果资源实现了 Closeable 接口,最好使用 try-with-resources 语句,它可以让你自动释放资源。下面演示了如何使用 Java 7 完成它:

try (Connection conn = DriverManager
               .getConnection("dburl", "username", "password");
     ResultSet rs = conn.createStatement()
               .executeQuery("select * from some_table")) {
    while (rs.next()) {
        //process the retrieved data
    }
} catch (SQLException ex) {
    //Do something
    //The exception was probably caused 
    //by incorrect SQL statement
}

前面的示例 创建数据库连接,检索数据并处理它,然后关闭(调用 close() 方法) connrs 对象。

Java 9 增强了 try-with-resources 语句的功能,允许创建表示 try 块,以及在 try-with-resources 语句中的使用,如下所示:

void method(Connection conn, ResultSet rs) {
    try (conn; rs) {
        while (rs.next()) {
            //process the retrieved data
        }
    } catch (SQLException ex) {
        //Do something
        //The exception was probably caused 
        //by incorrect SQL statement
    }
}

前面的代码看起来更简洁,尽管在 实践中,程序员更喜欢在同一上下文中创建和释放(关闭)资源。如果这也是您的偏好,请考虑将 throws 语句与 try-with-resources 语句结合使用。

The throws statement

我们必须处理 SQLException 因为它是一个检查异常,并且 getConnection(), createStatement()executeQuery()next() 方法声明 在他们的 throws 子句中。这是一个例子:

Statement createStatement() throws SQLException;

这意味着方法的作者警告方法的用户它可能会抛出这样的异常,迫使他们要么捕获异常,要么在其方法的 throws 子句中声明它。在前面的示例中,我们选择使用两个 try...catch 语句来捕获它。或者,我们可以在 throws 子句中列出异常,从而通过有效地将异常处理的负担推给我们方法的用户来消除混乱:

void throwsDemo() throws SQLException {
    Connection conn = 
      DriverManager.getConnection("url","user","pass");
    ResultSet rs = conn.createStatement().executeQuery(
      "select * ...");
    try (conn; rs) {
        while (rs.next()) {
            //process the retrieved data
        }
    } finally { 
        try {
           if(conn != null) {
              conn.close();
           }
        } finally {
           if(rs != null) {
               rs.close();
           }
        }
    }
}

我们去掉了 catch 子句,但是我们需要 finally 块来关闭创建的 conn 和 rs 对象。

请注意我们如何将关闭 conn 对象的代码包含在 try 块中,以及我们如何将关闭 rs 对象的代码包含在 finally 块中。这样我们可以确保关闭 conn 对象期间的异常不会阻止我们关闭 rs 对象。

这段代码看起来不如我们在上一节中演示的 try-with-resources 语句清晰。我们展示它只是为了展示所有可能性以及如何避免可能的危险(不关闭资源)如果你决定自己做,而不是让 try-with-resources 语句自动为你做。

但是让我们回到 throws 语句的讨论上来。

throws 子句允许但不要求我们列出未经检查的异常。添加未经检查的异常不会强制方法的用户处理它们。

最后,如果方法抛出几个不同的异常,可以列出基本的Exception异常类而不是全部列出.这将使编译器高兴;但是,这并不是一个好的做法,因为它隐藏了方法用户可能期望的特定异常的细节。

请注意,编译器不会检查方法体中的代码会抛出什么样的异常。因此,可以在 throws 子句中列出任何异常,这可能会导致不必要的开销。如果程序员错误地在 throws 子句中包含了该方法从未实际抛出的已检查异常,则该方法的用户可以编写一个 catch 块永远不会被执行。

The throw statement

throw 语句允许 抛出程序员认为必要的任何异常。您甚至可以创建自己的异常。要创建检查异常,请按如下方式扩展 java.lang.Exception 类:

class MyCheckedException extends Exception{
    public MyCheckedException(String message){
        super(message);
    }
    //add code you need to have here
}

此外,要创建未经检查的异常,请扩展 java.lang.RunitmeException 类,如下所示:

class MyUncheckedException extends RuntimeException{
    public MyUncheckedException(String message){
        super(message);
    }
    //add code you need to have here
}

注意 add code you need to have here 注释。您可以像使用任何其他常规类一样向自定义异常添加方法和属性,但程序员很少这样做。事实上,最佳实践明确建议避免使用异常来驱动业务逻辑。 Exceptions 应该是顾名思义,仅涵盖异常或非常罕见的情况。

但是,如果您需要宣布异常情况,请使用 throw 关键字和 new 运算符创建并触发异常对象的传播。这里有一些例子:

throw new Exception("Something happened"); 
throw new RuntimeException("Something happened");
throw new MyCheckedException("Something happened");
throw new MyUncheckedException("Something happened");

甚至可以抛出 null,如下:

throw null;

上一条语句的结果与这条语句的结果相同:

throw new NullPointerException;

在这两种情况下,未经检查的 NullPointerException 异常的对象开始在系统中传播,直到它被应用程序或 JVM 捕获。

The assert statement

有时,程序员需要知道代码中是否发生了特定情况,即使在应用程序已经部署到生产环境之后也是如此。同时,无需一直运行此检查。这就是 assert 分支语句派上用场的地方。这是一个例子:

public someMethod(String s){
    //any code goes here
    assert(assertSomething(x, y, z));
    //any code goes here
}
boolean assertSomething(int x, String y, double z){
    //do something and return boolean
}

在前面的代码中,assert() 方法从 assertSomething() 方法获取输入。如果 assertSomething() 方法返回 false,则程序停止执行。

assert() 方法仅在使用 -ea< 运行 JVM 时执行 /code> 选项。 -ea 标志不应在生产中使用,除非可能暂时用于测试目的。这是因为它会产生影响应用程序性能的开销。

Best practices of exception handling

已检查异常 旨在用于当应用程序可以自动执行某些操作来修正或解决问题时的可恢复条件。在实践中,这种情况并不经常发生。通常,当捕获到异常时,应用程序会记录堆栈跟踪并中止当前操作。根据记录的信息,应用程序支持团队修改代码以解决下落不明的情况或防止将来发生这种情况。

每个应用程序都是不同的,因此最佳实践取决于特定的应用程序要求、设计和上下文。总的来说,开发社区似乎达成了一项协议,以避免使用受检异常并尽量减少它们在应用程序代码中的传播。以下列出了一些已被证明有用的其他建议:

  • 始终捕获靠近源的所有已检查异常。
  • 如果有疑问,请捕获也靠近源的未经检查的异常。
  • 尽可能靠近源处理异常,因为这是上下文最具体的地方,也是根本原因所在的地方。
  • 不要抛出已检查的异常,除非你必须这样做,因为你会为可能永远不会发生的情况强制构建额外的代码。
  • 将第三方检查的异常转换为未经检查的异常,如果需要,通过将它们重新抛出为 RuntimeException 以及相应的消息。
  • 除非必须,否则不要创建自定义异常。
  • 除非必须,否则不要使用异常处理机制来驱动业务逻辑。
  • 通过使用消息系统和可选的 enum 类型来自定义通用 RuntimeException 异常,而不是使用异常类型来传达错误的原因。

还有许多其他 可能的提示和建议;然而,如果你遵循这些,你可能会在绝大多数情况下没问题。至此,我们结束本章。

Summary

本章介绍了 Java 异常处理框架,了解了两种异常——已检查和未检查(运行时),以及如何使用 try-catch-finally< /code> 和 throws 语句。此外,您还学习了如何生成(抛出)异常以及如何创建自己的(自定义)异常。本章以异常处理的最佳实践结束,如果始终遵循这些实践,将帮助您编写干净清晰的代码,这些代码编写起来令人愉快,易于理解和维护。

在下一章中,我们将详细讨论字符串及其处理,以及输入/输出流和文件读写技术。

Quiz

  1. 什么是堆栈跟踪?选择所有符合条件的:
    1. 当前加载的类列表
    2. 当前正在执行的方法列表
    3. 当前执行的代码行列表
    4. 当前使用的变量列表
  2. 有哪些例外?选择所有符合条件的:
    1. 编译异常
    2. 运行时异常
    3. 读取异常
    4. 写异常
  3. 以下代码的输出是什么?
    试试{     抛出null; } 捕捉(运行时异常前){     System.out.print("RuntimeException"); } 捕捉(异常前){     System.out.print("异常"); } 捕捉(错误前){     System.out.print("错误"); } 捕捉(可投掷的前){     System.out.print("Throwable"); } 最后 {     System.out.println("终于"); }
    1. 一个RuntimeException错误
    2. 终于出现异常错误
    3. RuntimeException 最后
    4. Throwable finally
  4. 以下哪种方法编译不会出错?
    void method1() throws Exception { throw null; } void method2() throws RuntimeException { throw null; } void method3() throws Throwable { throw null; } void method4() 抛出错误 { throw null; }
    1. method1()
    2. method2()
    3. method3()
    4. method4()
  5. 以下哪个语句编译不会出错?
    throw new NullPointerException("Hi there!"); //1 throws new Exception("你好!");          //2 throw RuntimeException("你好!");       //3 throws RuntimeException("Hi there!");     //4
    1. 1
    2. 2
    3. 3
    4. 4
  6. 假设 int x = 4,以下哪个语句编译不会出错?
    assert (x > 3); //1 断言(x = 3); //2 断言(x<4); //3 断言(x = 4); //4
    1. 1
    2. 2
    3. 3
    4. 4
  7. 以下列表中的最佳实践是哪些?
    1. 始终捕获所有异常和错误。
    2. 始终捕获所有异常。
    3. 永远不要抛出未经检查的异常。
    4. 除非迫不得已,否则尽量不要抛出已检查的异常。