Java异常处理详解
异常概述
异常是程序运行过程中发生的不正常事件,它中断了正常的指令流。Java使用面向对象的方式来处理异常,通过异常机制可以优雅地处理程序中的错误情况,提高程序的健壮性。
异常体系结构
Java的异常体系以Throwable类为根,派生出两大子类:Error和Exception。
异常层次结构
Throwable
├── Error
│ ├── VirtualMachineError
│ │ ├── OutOfMemoryError
│ │ └── StackOverflowError
│ └── LinkageError
└── Exception
├── RuntimeException
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ClassCastException
│ ├── ArithmeticException
│ └── IllegalArgumentException
├── IOException
│ ├── FileNotFoundException
│ └── EOFException
├── SQLException
└── ClassNotFoundExceptionError与Exception的区别
- Error:表示严重错误,程序一般无法恢复,如JVM内存溢出、栈溢出等。不建议捕获和处理。
- Exception:表示可以处理的异常,是我们在编程中主要关注的部分。
- Checked Exception(受检异常):必须显式捕获或声明抛出,如
IOException、SQLException。 - Unchecked Exception(非受检异常):可以不显式处理,通常是程序逻辑错误,如
NullPointerException、ArrayIndexOutOfBoundsException。
- Checked Exception(受检异常):必须显式捕获或声明抛出,如
异常处理机制
try-catch-finally 语句
基本语法:
java
try {
// 可能抛出异常的代码
} catch (ExceptionType1 e1) {
// 处理ExceptionType1类型的异常
} catch (ExceptionType2 e2) {
// 处理ExceptionType2类型的异常
} finally {
// 无论是否发生异常都会执行的代码
}示例:
java
public class ExceptionExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("结果: " + result);
} catch (ArithmeticException e) {
System.out.println("捕获到算术异常: " + e.getMessage());
} finally {
System.out.println("finally块一定会执行");
}
}
public static int divide(int a, int b) {
return a / b;
}
}执行结果:
捕获到算术异常: / by zero
finally块一定会执行多重catch块
当可能抛出多种类型的异常时,可以使用多个catch块来分别处理。
注意事项:
- 异常捕获的顺序很重要,应该先捕获子类异常,再捕获父类异常
- 否则子类异常的catch块永远不会被执行
示例:
java
try {
String str = null;
System.out.println(str.length());
int[] arr = new int[5];
arr[10] = 100;
} catch (NullPointerException e) {
System.out.println("空指针异常: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界异常: " + e.getMessage());
} catch (Exception e) {
System.out.println("其他异常: " + e.getMessage());
}try-with-resources 语句
Java 7引入的try-with-resources语句用于自动关闭实现了AutoCloseable接口的资源,无需在finally块中手动关闭。
语法:
java
try (ResourceType resource = new ResourceType();
AnotherResourceType anotherResource = new AnotherResourceType()) {
// 使用资源的代码
} catch (Exception e) {
// 异常处理
}示例:
java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("读取文件异常: " + e.getMessage());
}
}
}throws 与 throw 关键字
throws 关键字
用于在方法声明中指定该方法可能抛出的异常类型,将异常责任传递给调用者。
语法:
java
public returnType methodName(paramList) throws ExceptionType1, ExceptionType2 {
// 方法体
}示例:
java
public class ThrowsExample {
public static void main(String[] args) {
try {
readFile("file.txt");
} catch (IOException e) {
System.out.println("处理异常: " + e.getMessage());
}
}
public static void readFile(String fileName) throws IOException {
// 可能抛出IOException的代码
}
}throw 关键字
用于在方法内部抛出一个具体的异常对象。
语法:
java
throw new ExceptionType("异常信息");示例:
java
public class ThrowExample {
public static void main(String[] args) {
try {
checkAge(15);
} catch (IllegalArgumentException e) {
System.out.println("年龄检查异常: " + e.getMessage());
}
}
public static void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("年龄必须大于等于18岁");
}
System.out.println("年龄检查通过");
}
}自定义异常
在实际开发中,我们可以创建自定义异常来处理特定业务场景下的异常情况。
创建自定义异常类
步骤:
- 继承
Exception类(受检异常)或RuntimeException类(非受检异常) - 提供构造方法
示例:
java
// 自定义受检异常
public class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(double amount) {
super("余额不足: 还需要 " + amount + " 元");
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
// 自定义非受检异常
public class InvalidAccountException extends RuntimeException {
public InvalidAccountException(String message) {
super(message);
}
}使用自定义异常
java
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}
public void transfer(BankAccount target, double amount, String targetAccountId) {
if (target == null) {
throw new InvalidAccountException("目标账户无效: " + targetAccountId);
}
try {
this.withdraw(amount);
target.balance += amount;
} catch (InsufficientFundsException e) {
System.out.println("转账失败: " + e.getMessage());
}
}
}异常链
异常链是指在捕获一个异常后,再抛出另一个异常,但同时保留原有异常的信息。这在处理高级异常时非常有用。
使用 initCause() 方法
java
try {
// 可能抛出SQLException的代码
} catch (SQLException e) {
// 创建新的业务异常,并设置原始异常为cause
BusinessException businessException = new BusinessException("数据库操作失败");
businessException.initCause(e);
throw businessException;
}在构造方法中设置 cause
java
// 自定义异常类,支持cause参数
public class BusinessException extends Exception {
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
// 使用
try {
// 可能抛出SQLException的代码
} catch (SQLException e) {
throw new BusinessException("数据库操作失败", e);
}异常链的传播
java
try {
methodA();
} catch (BusinessException e) {
System.out.println("业务异常: " + e.getMessage());
System.out.println("根本原因: " + e.getCause().getMessage());
}
public static void methodA() throws BusinessException {
try {
methodB();
} catch (DataAccessException e) {
throw new BusinessException("业务处理失败", e);
}
}
public static void methodB() throws DataAccessException {
try {
// 数据库操作
} catch (SQLException e) {
throw new DataAccessException("数据访问失败", e);
}
}异常处理最佳实践
1. 只捕获真正能处理的异常
java
// 不推荐
try {
// 复杂操作
} catch (Exception e) {
// 过于宽泛的异常捕获
System.out.println("发生错误");
}
// 推荐
try {
// 文件操作
} catch (IOException e) {
// 具体的异常处理
log.error("文件读取失败", e);
// 提供恢复机制或合理的默认行为
}2. 避免空的catch块
空的catch块会隐藏错误,使调试变得困难。
java
// 不推荐
try {
// 可能抛出异常的代码
} catch (Exception e) {
// 空的catch块
}
// 推荐
try {
// 可能抛出异常的代码
} catch (Exception e) {
// 至少记录异常信息
log.error("处理失败", e);
}3. 尽早抛出,延迟捕获
- 尽早抛出异常:在发现错误时立即抛出
- 延迟捕获异常:在能有效处理异常的地方再捕获
4. 关闭资源
总是确保资源被正确关闭,使用try-with-resources是最佳选择。
5. 提供有用的异常信息
异常信息应该清晰描述问题,便于调试。
java
// 不推荐
throw new Exception("错误");
// 推荐
throw new IllegalArgumentException("用户名必须包含至少5个字符,当前: " + username.length());6. 避免过度使用异常
异常处理的开销较大,不要用异常来控制正常的程序流程。
java
// 不推荐
try {
int index = list.indexOf(element);
if (index == -1) {
throw new ElementNotFoundException("元素未找到");
}
} catch (ElementNotFoundException e) {
// 处理元素未找到的情况
}
// 推荐
int index = list.indexOf(element);
if (index == -1) {
// 直接处理元素未找到的情况
return defaultValue;
}7. 区分Checked和Unchecked异常
- Checked异常:用于可恢复的错误,如资源不可用、业务规则违反等
- Unchecked异常:用于编程错误,如空指针、数组越界等
8. 不要忽略异常
即使知道某些异常不太可能发生,也不要完全忽略它们。
常见异常类型
RuntimeException子类
| 异常类型 | 说明 | 常见原因 |
|---|---|---|
| NullPointerException | 空指针异常 | 尝试访问null对象的属性或方法 |
| ArrayIndexOutOfBoundsException | 数组索引越界 | 使用超出数组范围的索引 |
| ClassCastException | 类型转换异常 | 尝试将对象转换为不兼容的类型 |
| ArithmeticException | 算术异常 | 除以零等算术错误 |
| IllegalArgumentException | 非法参数异常 | 传入方法的参数不符合要求 |
| IllegalStateException | 非法状态异常 | 对象处于不适合执行请求操作的状态 |
| IndexOutOfBoundsException | 索引越界异常 | 集合、字符串等索引超出范围 |
| ConcurrentModificationException | 并发修改异常 | 在迭代集合时修改集合 |
Checked Exception子类
| 异常类型 | 说明 | 常见原因 |
|---|---|---|
| IOException | 输入输出异常 | 文件读写、网络通信等IO操作失败 |
| FileNotFoundException | 文件未找到异常 | 尝试访问不存在的文件 |
| SQLException | SQL异常 | 数据库操作失败 |
| ClassNotFoundException | 类未找到异常 | 尝试加载不存在的类 |
| InterruptedException | 线程中断异常 | 线程在睡眠或等待时被中断 |
| ParseException | 解析异常 | 解析字符串为其他类型失败 |
| UnsupportedEncodingException | 不支持的编码异常 | 使用不支持的字符编码 |
异常处理与日志记录
异常处理与日志记录密切相关,记录异常信息有助于调试和监控系统。
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExceptionLoggingExample {
private static final Logger logger = LoggerFactory.getLogger(ExceptionLoggingExample.class);
public void process() {
try {
// 业务逻辑
} catch (IOException e) {
// 记录异常信息,包括堆栈跟踪
logger.error("处理文件时发生IO异常", e);
// 重新抛出或处理异常
throw new BusinessException("文件处理失败", e);
} catch (Exception e) {
// 记录未预期的异常
logger.error("发生未预期的异常", e);
// 处理或向上传递
}
}
}总结
异常处理是Java编程中不可或缺的一部分,合理地使用异常处理机制可以使程序更加健壮和可维护。通过本文的学习,我们应该掌握:
- Java异常体系的结构
- 异常处理的基本语法(try-catch-finally, try-with-resources)
- throws和throw关键字的使用
- 如何创建和使用自定义异常
- 异常链的概念和应用
- 异常处理的最佳实践
在实际开发中,应该根据具体场景选择合适的异常处理策略,既要保证程序的健壮性,也要兼顾代码的可读性和性能。