异常层次结构
Java 中所有异常都继承自 Throwable 类,分为两大分支:Error 和 Exception。
Throwable
├── Error (系统级错误,不可处理)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── NoClassDefFoundError
└── Exception (程序级异常,可处理)
├── IOException (受检异常)
├── SQLException (受检异常)
└── RuntimeException (非受检异常)
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ClassCastException
└── IllegalArgumentException
- Error:JVM 层面的错误,程序无法处理,也不应尝试捕获
- Exception:程序可以捕获并处理的异常
受检异常 vs 非受检异常
| 类型 | 父类 | 编译器检查 | 典型场景 |
|---|---|---|---|
| 受检异常 (Checked) | Exception (非 RuntimeException) | 必须显式处理 | IO操作、数据库操作 |
| 非受检异常 (Unchecked) | RuntimeException | 不强制处理 | 空指针、数组越界 |
// 受检异常:必须 try-catch 或 throws
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
}
// 非受检异常:编译器不强制要求处理
public int divide(int a, int b) {
return a / b; // 如果 b=0 抛出 ArithmeticException
}
try-catch-finally
最基础的异常处理结构:
public void demo() {
try {
int[] arr = new int[5];
arr[10] = 1; // 抛出异常
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界:" + e.getMessage());
} finally {
System.out.println("无论如何都会执行");
}
}
多个 catch 块的匹配规则:从上到下匹配,子类异常必须写在父类异常前面:
try {
// 业务代码
} catch (FileNotFoundException e) {
// 子类异常在前
} catch (IOException e) {
// 父类异常在后
} catch (Exception e) {
// 兜底
}
finally 的执行时机——即使在 try 或 catch 中执行了 return,finally 仍会执行:
public static int test() {
try {
return 1;
} finally {
System.out.println("finally 执行了");
}
}
// 输出:finally 执行了
// 返回值:1
唯一不执行 finally 的情况:调用了 System.exit(0) 或 JVM 崩溃。
try-with-resources (JDK 7+)
自动关闭实现了 AutoCloseable 接口的资源,无需手动写 finally:
// 传统方式
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("test.txt"));
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// try-with-resources 方式(简洁且安全)
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
多个资源可以同时声明,用分号分隔:
try (FileInputStream fis = new FileInputStream("in.txt");
FileOutputStream fos = new FileOutputStream("out.txt")) {
// 读写操作
// 两个流都会自动关闭(关闭顺序与声明顺序相反)
}
throw vs throws
| 关键字 | 位置 | 作用 |
|---|---|---|
throw |
方法体内部 | 抛出一个异常对象 |
throws |
方法签名上 | 声明方法可能抛出的异常类型 |
// throws:声明可能抛出的异常
public void checkAge(int age) throws IllegalArgumentException {
if (age < 0) {
// throw:实际抛出异常对象
throw new IllegalArgumentException("年龄不能为负数:" + age);
}
}
自定义异常
// 自定义受检异常
public class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}
// 自定义非受检异常
public class BusinessException extends RuntimeException {
private String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
异常链
将一个异常作为另一个异常的原因,保留完整的调用栈:
try {
// 数据库操作
} catch (SQLException e) {
// 包装为业务异常并保留原始异常
throw new BusinessException("DB001", "数据查询失败", e);
}
最佳实践
- 不要吞掉异常——永远不要写空的 catch 块:
// 反例
try {
doSomething();
} catch (Exception e) {
// 什么都不做,问题被隐藏
}
// 正例:至少记录日志
try {
doSomething();
} catch (Exception e) {
log.error("处理失败", e);
throw new ServiceException("操作失败", e);
}
优先使用标准异常——能用 JDK 自带的就别自定义(如
IllegalArgumentException、IllegalStateException)异常信息要明确——包含关键参数和上下文:
throw new IllegalArgumentException("用户ID无效: " + userId);
尽早抛出,延迟捕获——底层发现问题立即抛出,在能处理的地方才捕获
不要在循环中使用 try-catch——把 try-catch 放在循环外面,避免性能损耗