Java 异常处理

2025 年 4 月 25 日 | 阅读 12 分钟

Java 中的异常处理是一种强大的机制,用于处理运行时错误,从而可以维持应用程序的正常流程。

在本节中,我们将学习 Java 异常、其类型以及已检查异常和未检查异常之间的区别。

Java 中的异常是什么?

在 Java 中,异常是在程序执行期间发生的事件,它会中断指令的正常流程。这些异常可能由于各种原因发生,例如无效的用户输入、找不到文件或除以零。当发生异常时,通常由 `java.lang.Exception` 类的一个子类对象表示。

什么是异常处理?

异常处理是一种处理运行时错误(如 `ClassNotFoundException`、`IOException`、`SQLException`、`RemoteException` 等)的机制。

异常处理的优势

异常处理的核心优势在于维持应用程序的正常流程。异常通常会中断应用程序的正常流程;这就是为什么我们需要处理异常。让我们考虑一个场景

假设一个 Java 程序有 10 条语句,并且在第 5 条语句处发生了一个异常;其余代码将不会执行,即第 6 到 10 条语句将不会执行。但是,当我们执行异常处理时,其余的语句将执行。这就是为什么我们在 Java 中使用异常处理。

Java 异常类层次结构

`java.lang.Throwable` 类是 Java 异常层次结构中由 `Exception` 和 `Error` 两个子类继承的根类。Java 异常类的层次结构如下所示

hierarchy of exception handling

Java 异常的类型

在 Java 中,异常分为两种主要类型:已检查异常和未检查异常。此外,还有第三种称为错误的类别。让我们深入了解这些类型

  1. 已检查异常
  2. 未检查异常
  3. Error
hierarchy of exception handling

1. 已检查异常

已检查异常是在编译时检查的异常。这意味着编译器会验证代码是否通过捕获它们或使用 `throws` 关键字在方法签名中声明它们来处理这些异常。已检查异常的示例包括

IOException: 当输入/输出操作失败时,例如从文件读取或写入文件时,会抛出异常。

SQLException: 访问数据库时发生错误时会抛出此异常。

ParseException: 表示在将字符串解析为其他数据类型(例如解析日期)时出现问题。

ClassNotFoundException: 当应用程序尝试通过其字符串名称(使用 `Class.forName()` 等方法)加载类时,但找不到具有指定名称的类(在类路径中)时,会抛出此异常。

2. 未检查异常 (运行时异常)

未检查异常,也称为运行时异常,在编译时不会进行检查。这些异常通常是由于编程错误引起的,例如代码中的逻辑错误或不正确的假设。它们不需要在方法签名中使用 `throws` 关键字声明,因此处理它们是可选的。未检查异常的示例包括

NullPointerException: 当尝试在为 null 的对象引用上访问或调用方法时,会抛出此异常。

ArrayIndexOutOfBoundsException: 当我们尝试使用无效索引访问数组元素时,会发生此异常。

ArithmeticException: 算术运算失败时(例如除以零)会抛出此异常。

IllegalArgumentException: 表示方法收到了非法或不恰当的参数。

3. 错误

错误表示在正常情况下不应捕获的异常情况。它们通常是由应用程序无法控制的问题引起的,例如系统故障或资源耗尽。应用程序代码不应捕获或处理错误。错误的示例包括

OutOfMemoryError: 当 Java 虚拟机 (JVM) 无法为应用程序分配足够的内存时,会发生此错误。

StackOverflowError: 由于过度递归导致栈内存耗尽时,会抛出此错误。

NoClassDefFoundError: 表示 JVM 找不到在编译时可用的类的定义。

理解 Java 中不同类型的异常对于编写健壮可靠的代码至关重要。通过适当地处理异常,可以提高应用程序的弹性并提供更好的用户体验。异常处理层次结构

Java 异常关键字

Java 提供了五个用于处理异常的关键字。下表描述了每个关键字。

关键字描述
try“try”关键字用于指定一个应该放置异常代码的块。这意味着我们不能单独使用 try 块。try 块后面必须跟 catch 或 finally。
catch“catch”块用于处理异常。它必须在 try 块之前,这意味着我们不能单独使用 catch 块。之后可以跟 finally 块。
finally“finally”块用于执行程序的必要代码。无论是否处理了异常,它都会被执行。
throw“throw”关键字用于抛出异常。
throws“throws”关键字用于声明异常。它表明方法中可能发生异常。它不抛出异常。它总是与方法签名一起使用。

Java 异常处理示例

让我们来看一个 Java 异常处理的示例,其中我们使用 try-catch 语句来处理异常。

示例

编译并运行

输出

Exception in thread main java.lang.ArithmeticException:/ by zero
rest of the code...

在上面的示例中,100/0 会引发一个 `ArithmeticException`,它由 try-catch 块处理。

try-catch 块

Java 中处理异常的主要机制之一是 try-catch 块。try 块包含可能抛出异常的代码,catch 块用于在异常发生时处理该异常。这是一个基本示例

处理多个异常

您可以通过提供多个 catch 块来处理多种类型的异常,每个块捕获不同类型的异常。这允许您根据抛出的具体异常类型来定制异常处理逻辑。这是一个示例

finally 块

除了 try 和 catch 之外,Java 还提供了 finally 块,它允许您执行清理代码(例如关闭资源),无论是否发生异常。finally 块通常用于释放在 try 块中获取的资源。这是一个示例

Java 异常的常见场景

以下是一些可能发生未检查异常的场景。它们如下

1) 发生 ArithmeticException 的场景

如果我们用零除任何数字,就会发生 `ArithmeticException`。

这是一个简单的 Java 代码示例,其中发生 `ArithmeticException`

示例

编译并运行

输出

Error: Division by zero is not allowed.

说明

我们有一个 `main()` 方法,其中我们尝试执行除以零的操作,这在算术中是不允许的。

在 try 块内部,我们执行除法运算 `dividend / divisor`,其中 `divisor` 的值为 0。

当发生除以零的情况时,会抛出 `ArithmeticException`。我们使用专门用于 `ArithmeticException` 的 catch 块来捕获此异常。

在 catch 块中,我们通过打印错误消息来处理异常,表明不允许除以零。如果需要,可以在此处添加其他错误处理逻辑。

2) 发生 NullPointerException 的场景

如果任何变量的值为 null,则对该变量执行任何操作都会抛出 `NullPointerException`。

这是一个 Java 代码示例,其中发生 `NullPointerException`

示例

编译并运行

输出

Error: Null reference encountered.

说明

我们有一个 `main()` 方法,其中我们将 String 变量 `str` 初始化为 null。

在 try 块内部,我们尝试在 `str` 引用上调用 `length()` 方法,该引用为 null。

当尝试在 null 引用上调用方法时,会抛出 `NullPointerException`。

我们使用专门用于 `NullPointerException` 的 catch 块来捕获此异常。

在 catch 块中,我们通过打印错误消息来处理异常,表明遇到了 null 引用。如果需要,可以在此处添加其他错误处理逻辑。

3) 发生 NumberFormatException 的场景

如果任何变量或数字的格式不匹配,可能会导致 `NumberFormatException`。假设我们有一个包含字符的字符串变量;将此变量转换为数字将导致 `NumberFormatException`。

这是一个 Java 代码示例,其中发生 `NumberFormatException`

示例

编译并运行

输出

Error: Unable to parse the string as an integer.

说明

我们有一个 `main()` 方法,其中我们将 String 变量 `str` 初始化为非数字字符。

在 try 块内部,我们尝试使用 `Integer.parseInt()` 方法将字符串 `str` 解析为整数。

由于字符串包含非数字字符,因此解析操作会抛出 `NumberFormatException`。

我们使用专门用于 `NumberFormatException` 的 catch 块来捕获此异常。

在 catch 块中,我们通过打印错误消息来处理异常,表明该字符串无法解析为整数。如果需要,可以在此处添加其他错误处理逻辑。

4) 发生 ArrayIndexOutOfBoundsException 的场景

当数组超出其大小时,会发生 `ArrayIndexOutOfBoundsException`。可能还有其他原因导致 `ArrayIndexOutOfBoundsException`。考虑以下语句。

这是一个 Java 代码示例,其中发生 `ArrayIndexOutOfBoundsException`

示例

编译并运行

输出

Error: Index is out of bounds.

说明

我们有一个 `main()` 方法,其中我们将数组 `numbers` 初始化为 5 个元素。

在 try 块内部,我们尝试访问索引 10 处的元素,该索引超出了数组 `numbers` 的范围。

由于索引超出了范围,因此会抛出 `ArrayIndexOutOfBoundsException`。

我们使用专门用于 `ArrayIndexOutOfBoundsException` 的 catch 块来捕获此异常。

在 catch 块中,我们通过打印错误消息来处理异常,表明索引超出了范围。如果需要,可以在此处添加其他错误处理逻辑。

异常处理最佳实践

捕获特定异常: 尽可能捕获特定异常,而不是捕获通用的 `Exception` 对象。这有助于提供更精确的错误处理,并使代码更易于理解和维护。

保持异常处理简单: 避免过于复杂的异常处理逻辑。保持 catch 块简洁,并专注于处理它们设计的特定异常。复杂的异常处理逻辑可能会使代码难以调试和维护。

记录异常: 处理异常时,请务必记录异常或错误消息。这有助于在生产环境中进行故障排除和诊断问题。

适当地抛出异常: 在必要时抛出异常,但要避免过度使用已检查异常。已检查异常应用于调用者可以合理预期处理的异常情况。

使用自定义异常: 为应用程序中的特定错误情况创建自定义异常类。这有助于提供有意义的错误消息,并使代码更具自我文档化

结论

异常处理是 Java 编程的一个基本方面。通过遵循最佳实践和采用有效的异常处理策略,您可以编写更健壮、可维护的 Java 应用程序。请记住捕获特定异常、保持异常处理简单、记录异常以进行调试、适当地抛出异常以及在需要时使用自定义异常。掌握异常处理将帮助您构建更可靠的软件,该软件能够优雅地处理错误和异常。


Java 异常处理选择题

1. 如果一个方法抛出了一个它没有声明的已检查异常,会发生什么?

  1. 编译错误
  2. 运行时错误
  3. 程序在没有任何错误的情况下终止
  4. 该方法会自动捕获异常
 

答案:a)

解释:如果一个方法抛出了一个它没有声明的已检查异常,会导致编译错误。


2. 关于 finally 块,以下哪项是正确的?

  1. 无论是否抛出异常,它总是在 try 块之后执行。
  2. 仅当发生异常时才执行。
  3. 它必须跟在 catch 块后面。
  4. 仅当未发生异常时才执行。
 

答案:a)

解释:无论是否抛出异常,finally 块总是在 try 块之后执行。


3. NullPointerException 是什么类型的异常?

  1. 已检查异常
  2. 未检查异常
  3. Error
  4. 以上都不是
 

答案:b)

解释:NullPointerException 是一个未检查异常,意味着它不需要被声明或捕获。


4. 关于自定义异常,以下哪项陈述是正确的?

  1. 自定义异常不能继承自其他异常。
  2. 自定义异常必须继承自 Throwable 类。
  3. 自定义异常必须继承自 Exception 或其子类。
  4. 自定义异常只能是未检查异常。
 

答案:c)

解释:自定义异常必须继承自 Exception 类或其子类之一。


5. Java 中的异常传播是什么?

  1. 创建和抛出自定义异常的过程
  2. 在单个 try-catch 块中处理多个异常的过程
  3. 将异常从一个方法传递到其调用方法的进程
  4. 在嵌套的 try-catch 块中捕获和处理异常的过程
 

答案:c)

解释:将异常从一个方法传递到其调用方法的进程