异常处理
异常处理
程序运行过程中,可能会出现错误,Java 使用异常机制来处理这些错误。
异常的概念与分类
异常就是程序运行过程中出现的不正常情况(错误),Java 把异常封装成对象,统一处理。
异常继承体系
flowchart BT
Throwable --> Error
Throwable --> Exception
Exception --> RuntimeException
Exception --> 非运行时异常(检查异常)
- Throwable:所有错误和异常的根类
- Error:错误,严重问题,JVM 层面问题,比如
OutOfMemoryError、StackOverflowError,程序无法处理,只能避免
- Exception:异常,程序可以处理
- RuntimeException:运行时异常,不检查,比如
NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException,一般是代码错误导致,需要改进代码避免
- 编译时异常(Checked Exception):编译时必须处理,否则编译不通过,比如
IOException、SQLException
处理编译时异常的方法有两种:
- try-catch捕获这个异常,throw RuntimeException 自己处理了这个异常,程序可以继续进行
- throws抛出异常,我不处理这个异常,向外抛出异常,外部没有处理会终止程序
| 对比维度 | 编译时异常(受检异常 CheckedException) | 运行时异常(非受检异常 UncheckedException) |
|---|---|---|
| 继承关系 | 直接 / 间接继承 Exception,不是RuntimeException子类 |
继承 RuntimeException(RuntimeException本身是Exception的子类) |
| 编译器检查 | 编译期强制校验抛出异常必须 try-catch 捕获 或 throws 声明 |
编译器不强制校验抛出异常无需声明 throws,可选择性捕获 |
| 出现时机 | 异常对象运行时抛出;只是编译阶段要求提前处理 | 异常对象运行时抛出;编译阶段无强制要求 |
| 设计初衷 | 可预期、大概率能够修复的异常(外部环境问题) | 程序代码 BUG、逻辑错误,一般无法在代码内恢复 |
| 代表场景 | 文件读写、网络、数据库、反射、日期解析 | 空指针、数组越界、类型强转失败、除 0 运算 |
| 自定义异常写法 | class MyException extends Exception{} |
class MyException extends RuntimeException{} |
| 企业开发推荐 | 极少使用 | 业务异常首选(SpringBoot 主流方案) |
编译时异常
| 异常名称 | 产生场景说明 |
|---|---|
IOException |
文件、流读写、网络 IO 操作异常(父类) |
FileNotFoundException |
读取文件时,目标文件不存在 |
SQLException |
JDBC 操作数据库发生异常 |
ParseException |
SimpleDateFormat日期字符串解析失败 |
ClassNotFoundException |
反射 Class.forName() 找不到目标类 |
NoSuchMethodException |
反射找不到指定方法 |
NoSuchFieldException |
反射找不到成员变量 |
InterruptedException |
sleep()/wait()线程被中断 |
CloneNotSupportedException |
对象调用 clone () 但未实现 Cloneable 接口 |
MalformedURLException |
URL 地址格式不合法 |
运行时异常:
| 异常类 | 说明 |
|---|---|
NullPointerException |
空指针异常,调用了 null 对象的方法或属性 |
ArrayIndexOutOfBoundsException |
数组索引越界异常 |
ClassCastException |
类型转换异常 |
IllegalArgumentException |
参数非法异常 |
ArithmeticException |
算术异常,比如除零 |
IllegalArgumentException |
传入非法参数 |
NumberFormatException |
字符串转数字格式错误(属于 IllegalArgumentException 子类) |
IndexOutOfBoundsException |
集合、数组索引越界父类 |
try-catch-finally
捕获异常,处理异常:
语法:
try {
// 可能出异常的代码
} catch (异常类型 变量) {
// 捕获到异常后的处理代码
} finally {
// 一定会执行的代码,用于释放资源
}
执行流程:
- try 中代码如果没有异常,跳过 catch,执行 finally
- try 中如果出现异常,跳转到 catch,执行 catch 中代码,然后执行 finally
示例:
public class Demo {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
// 这里会发生索引越界
System.out.println(arr[10]);
} catch (ArrayIndexOutOfBoundsException e) {
// 捕获异常,处理
System.out.println("索引越界了:" + e.getMessage());
// 打印异常堆栈信息,方便调试
e.printStackTrace();
} finally {
System.out.println("finally 一定会执行");
}
System.out.println("程序继续执行");
}
}
💡 如果没有 try-catch,异常会向上抛,最终到 JVM,JVM 会打印异常信息然后终止程序。用了 try-catch,程序可以继续执行。
try-catch 多种异常
可以多个 catch 捕获不同异常:
try {
// ...
} catch (ArrayIndexOutOfBoundsException e) {
// 处理索引越界
} catch (NullPointerException e) {
// 处理空指针
} catch (Exception e) {
// 捕获其他异常,放最后
}
Java 7+ 支持多异常同 catch:
try {
// ...
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
// 两种异常同处理
}
finally 的作用
finally 用来释放资源,因为 finally 一定会执行,不管有没有异常:
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 读取文件...
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流,释放资源,一定会执行
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java 7+ 提供了 try-with-resources 语法,自动关闭资源,更简洁:
try (FileInputStream fis = new FileInputStream("test.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {
// 读取文件...
} catch (IOException e) {
e.printStackTrace();
}
// 自动关闭资源,不需要写 finally
💡 推荐使用 try-with-resources,代码更简洁,不容易出错。
throws 与 throw
throws 声明抛出异常
方法内部可能出异常,自己不处理,声明抛出,让调用者处理:
// 方法声明上 throws 异常
public void readFile(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
// ...
}
调用这个方法时,必须处理这个异常:要么继续 throws,要么 try-catch。
throw 手动抛出异常
我们可以手动抛出异常对象:
public void setAge(int age) {
if (age < 0 || age > 150) {
// 手动抛出异常
throw new IllegalArgumentException("年龄不合法:" + age);
}
this.age = age;
}
throw 抛出的是一个异常对象。
对比 throws 和 throw:
| throw | throws |
|---|---|
| 方法内部,抛出一个异常对象 | 方法声明上,声明抛出哪些异常 |
throw new 异常对象() |
throws 异常类型1, 异常类型2 |
自定义异常
如果需要,我们可以自定义异常类:
步骤:
- 继承
Exception或RuntimeException - 提供无参构造
- 提供有参构造(接收错误信息)
继承
Exception时要受检要主动声明异常throws MeiQianException继承
RuntimeException时为非受检,可以不用声明
@Test
public void test4() {
try {
meiqian(1100);
} catch (MeiQianException e) {
e.printStackTrace();
}
}
public void meiqian(int money) throws MeiQianException {
if(money >= 1000){
throw new MeiQianException("余额不足");
}else {
System.out.println("余额充足");
}
}
// 自定义业务异常
public class MeiQianException extends RuntimeException {
public MeiQianException() {
super();
}
public MeiQianException(String message) {
super(message);
}
public MeiQianException(String message, Throwable cause) {
super(message, cause);
}
}
使用:
if (余额不足) {
throw new BusinessException("余额不足,无法支付");
}
异常链与日志
捕获底层异常后,如果要抛出更符合业务语义的新异常,应保留原始异常作为 cause,形成异常链:
try {
repository.save(order);
} catch (IOException e) {
throw new BusinessException("保存订单失败", e);
}
这样上层既能看到业务错误信息,也能通过 cause 追踪到底层原因。
在真实项目中,不建议到处使用 e.printStackTrace();更常见做法是使用日志框架记录异常:
// 示例写法,具体日志框架在后续章节介绍
logger.error("保存订单失败,orderId={}", orderId, e);
异常处理最佳实践
- 异常不能被吞掉:catch 块不要空着什么都不做,至少打印异常堆栈:
} catch (Exception e) {
// 不好:什么都不做,出了问题找不到
// 好:至少打印
e.printStackTrace();
}
- 不要用异常控制流程:异常是处理错误情况,不要用异常来做条件判断,性能差
- 尽早抛出异常:发现问题就立即抛出
- 分层处理:底层抛,上层捕获处理,UI 层提示用户,日志记录
- 优先使用 try-with-resources:关闭资源更方便
- 保留异常链:包装异常时把原始异常作为 cause 传入,不要丢失根因
- 合适的粒度:不要把一大段代码都放在 try 里面,只把可能出异常的代码放进去