Skip to content

Java 异常处理

一个异常 (exception) 是在程序执行期间发生、会中断正常指令流程的事件。Java 的异常处理机制提供了一种健壮的方法来管理这些事件。

常见的异常原因:

  • 无效的用户输入 (例如,在期望数字的地方输入文本)。
  • 资源问题 (例如,找不到文件、网络连接丢失、数据库不可用)。
  • 编程错误 (例如,访问数组越界、尝试使用 null 引用)。
  • 资源耗尽 (例如,内存不足)。

Java 将可抛出对象 (java.lang.Throwable 的子类) 分为三类:

  • 受检异常 (Checked Exceptions): 是 Exception 的子类 (不包括 RuntimeException 及其子类)。它们代表那些良好编写的应用程序应该预料到并从中恢复的异常情况 (例如,IOException、FileNotFoundException、SQLException)。Java 编译器要求你处理受检异常,要么通过 try-catch 块捕获它们,要么在方法签名中使用 throws 关键字声明它们 (“处理或声明规则”)。
  • 非受检异常 (Unchecked Exceptions) (运行时异常 Runtime Exceptions): 是 RuntimeException 的子类。它们通常表示编程错误 (例如,NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException、ClassCastException)。编译器不要求你处理或声明非受检异常,但你可以根据需要捕获它们。
  • 错误 (Errors): 是 Error 的子类。它们代表通常超出应用程序控制范围的严重问题,并且通常无法从中恢复 (例如,OutOfMemoryError、StackOverflowError、NoClassDefFoundError)。应用程序通常不应尝试捕获 Error。

所有异常和错误类型都继承自 java.lang.Throwable 类。

`Throwable`
├── `Error` (不可恢复的问题,通常不捕获)
│ ├── `VirtualMachineError` (例如,`OutOfMemoryError`、`StackOverflowError`)
│ └── ... (其他关键错误)
└── `Exception` (应用程序可能想要捕获的情况)
├── `IOException` (受检 - 与 I/O 失败相关)
│ └── `FileNotFoundException` (受检)
├── `SQLException` (受检 - 与数据库访问相关)
├── `RuntimeException` (非受检 - 编程错误或意外情况)
│ ├── `NullPointerException`
│ ├── `ArrayIndexOutOfBoundsException`
│ ├── `IllegalArgumentException`
│ ├── `ClassCastException`
│ └── ... (其他运行时异常)
└── ... (其他受检异常,如 ClassNotFoundException)

任何异常或错误对象上可用的常用方法:

方法描述
String getMessage()返回描述异常的详细信息字符串。
String toString()返回关于 throwable 的简短描述 (类名 + getMessage() 的结果)。
void printStackTrace()将 throwable 的堆栈跟踪 (导致异常的方法调用序列) 打印到标准错误流。对于调试非常有用。
void printStackTrace(PrintStream s) / void printStackTrace(PrintWriter s)将堆栈跟踪打印到指定的流/写入器。
StackTraceElement[] getStackTrace()返回堆栈跟踪元素的数组,允许以编程方式访问堆栈信息。
Throwable getCause()返回此 throwable 的根本原因 (如果指定了,用于异常链),否则返回 null。

Java 提供了关键字来处理异常:

  • try: 包含可能抛出异常的代码块 (受保护的代码)。
  • catch: 紧跟在 try 块后面。用于捕获并处理特定类型的异常。一个 try 块可以有多个 catch 块来处理不同类型的异常。
  • finally: 紧跟在 try 块和所有 catch 块后面。finally 块中的代码总是执行,无论 try 块中是否抛出或捕获到异常。它通常用于清理操作 (例如关闭文件或网络连接),尽管现在通常优先使用 try-with-resources 来管理资源。

基本语法:

try {
// 可能抛出一个或多个异常的代码
// ...
} catch (SpecificExceptionType1 e1) {
// 处理 SpecificExceptionType1
System.err.println("Caught exception 1: " + e1.getMessage());
// e1.printStackTrace(); // 通常用于调试
} catch (SpecificExceptionType2 e2) {
// 处理 SpecificExceptionType2
} catch (Exception e) {
// 最后捕获更广泛的异常 (如 Exception)
// 处理上面未捕获的任何其他 Exception 子类
} finally {
// 总是执行的清理代码
// 例如,关闭资源 (如果未使用 try-with-resources)
System.out.println("Finally block executed.");
}
// 这里的代码在 try-catch-finally 完成后执行 (除非程序退出)

示例:捕获 ArrayIndexOutOfBoundsException

Section titled “示例:捕获 ArrayIndexOutOfBoundsException”

此代码尝试访问无效的数组索引,从而导致异常。

// 文件: ExceptionDemo.java
public class ExceptionDemo {
public static void main(String[] args) {
try {
int[] numbers = new int[2]; // 数组索引为 0 和 1
System.out.println("Accessing element at index 3...");
numbers[3] = 10; // 此行将抛出异常
System.out.println("This line will not be executed.");
} catch (ArrayIndexOutOfBoundsException e) {
// 捕获特定异常
System.err.println("Error: Array index is out of bounds!");
System.err.println("Exception details: " + e); // 使用 toString()
// e.printStackTrace(); // 取消注释以获取完整的堆栈跟踪
} finally {
System.out.println("The 'finally' block is always executed.");
}
System.out.println("Program continues after the try-catch-finally block.");
}
}

输出:

Accessing element at index 3...
Error: Array index is out of bounds!
Exception details: java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 2
The 'finally' block is always executed.
Program continues after the try-catch-finally block.

多个 catch 块和多重捕获 (Multi-Catch)

Section titled “多个 catch 块和多重捕获 (Multi-Catch)”

你可以有多个 catch 块来处理不同类型的异常。第一个其参数类型与抛出的异常类型匹配 (或是其超类) 的 catch 块将被执行。顺序很重要:先捕获更具体的异常,后捕获更一般的异常。

Java 7 引入了多重捕获 (multi-catch),允许你在同一个块中捕获多种异常类型,如果它们共享相同的处理逻辑:

try {
// 可能抛出 IOException 或 SQLException 的代码
} catch (IOException | SQLException ex) {
// 在这里处理 IOException 和 SQLException
logger.error("I/O or SQL error: ", ex);
// 注意:在多重捕获块中,'ex' 隐式为 final
}

Java 7 引入了 try-with-resources 语句,它会在 try 块完成时自动关闭资源 (实现了 java.lang.AutoCloseable 或 java.io.Closeable 接口的对象,如流、套接字、数据库连接),无论是否发生异常。这是管理资源的首选方式,因为它比使用 finally 块进行关闭更简洁、更不容易出错。

语法:

// 在 try 后面的括号中声明资源
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
// 使用资源 (fis, reader)
String line = reader.readLine();
// ... 处理文件 ...
} catch (IOException e) {
// 处理在使用或关闭资源期间可能发生的异常
System.err.println("Error processing file: " + e.getMessage());
}
// 'reader' 和 'fis' 在此处自动关闭,即使发生异常。

如果一个方法可能抛出受检异常 (checked exception) 但没有使用 try-catch 处理它,则必须使用 throws 关键字在其方法签名中声明它可能抛出该异常。这会通知调用者他们需要自己处理或声明该异常 (即“处理或声明规则”)。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class FileReaderUtil {
// 此方法声明它可以抛出 IOException
public List<String> readFile(String filePath) throws IOException {
// Files.readAllLines() 可能抛出 IOException (一个受检异常)
return Files.readAllLines(Paths.get(filePath));
}
public void processFile() {
try {
List<String> lines = readFile("data.txt"); // 调用者必须处理 IOException
// ... 处理行 ...
} catch (IOException e) {
System.err.println("Failed to read file: " + e.getMessage());
}
}
}

一个方法可以声明多个异常,用逗号分隔 (例如,throws IOException, SQLException)。你通常不需要使用 throws 声明 RuntimeException 或 Error。

你可以使用 throw 关键字显式地抛出一个异常对象。这通常用于在你代码中发现错误条件时发出信号。

public void setAge(int age) {
if (age < 0) {
// 为无效参数抛出非受检异常
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
this.age = age;
}

你可以抛出新创建的异常对象,或者重新抛出在 catch 块中捕获的异常。

你可以通过扩展 Exception (用于受检异常) 或 RuntimeException (用于非受检异常) 来创建自己的自定义异常类。这使你可以创建与应用程序领域相关的特定异常类型。

自定义异常指南:

  • 对于你期望调用者能够合理恢复的情况,扩展 Exception (受检异常)。
  • 对于编程错误或意外情况,扩展 RuntimeException (非受检异常)。
  • 遵循标准的命名约定 (例如,类名以 Exception 结尾)。
  • 提供与超类构造函数匹配的构造函数 (例如,无参构造函数,一个接受消息字符串的构造函数,一个接受消息和原因 throwable 的构造函数)。
  • 如果自定义异常需要可序列化,请包含 serialVersionUID。
// 文件: InsufficientFundsException.java
public class InsufficientFundsException extends Exception { // 受检异常
private static final long serialVersionUID = 1L;
private final double shortfall;
public InsufficientFundsException(String message, double shortfall) {
super(message); // 将消息传递给 Exception 构造函数
this.shortfall = shortfall;
}
public double getShortfall() {
return shortfall;
}
}
// 文件: BankAccount.java
public class BankAccount {
private double balance;
// ... 构造函数,存款 ...
public void withdraw(double amount) throws InsufficientFundsException {
if (amount < 0) {
throw new IllegalArgumentException("Withdrawal amount cannot be negative.");
}
if (balance >= amount) {
balance -= amount;
} else {
double needs = amount - balance;
throw new InsufficientFundsException("Insufficient funds for withdrawal", needs);
}
}
// ... 获取余额 ...
}
// 用法:
try {
myAccount.withdraw(1000.00);
} catch (InsufficientFundsException e) {
System.err.println(e.getMessage() + ". Short by: " + e.getShortfall());
} catch (IllegalArgumentException iae) {
System.err.println("Invalid withdrawal amount: " + iae.getMessage());
}
  • 对可恢复的情况使用受检异常,对编程错误使用非受检异常。
  • 除非绝对必要且你知道如何处理,否则不要捕获 Throwable、Error 或过于宽泛的异常,如 Exception。
  • 使用 try-with-resources 来管理资源。
  • 在异常中提供有意义的消息。
  • 使用异常链 (new CustomException("message", cause)) 来在包装异常时保留原始异常上下文。
  • 不要忽略异常 (空的 catch 块通常是糟糕的做法)。记录它们或适当地处理它们。
  • 仅在无法使用 try-with-resources 时,才在 finally 块中清理资源。
  • 在某些情况下,考虑使用 Optional<T> (Java 8+) 作为返回 null 或抛出 NullPointerException 的替代方案。