Java的异常体系是所有异常处理逻辑的基础,整个体系以Throwable为顶层父类,向下分为Error和Exception两大分支。Error一般表示系统级错误,比如OutOfMemoryError,这类错误通常由JVM抛出,开发者不需要主动处理。Exception则是开发者需要重点关注的异常类型,它又分为受检异常Checked Exception和非受检异常Unchecked Exception,后者是RuntimeException及其子类。

Java异常类的核心分类
受检异常Checked Exception
受检异常是指编译器强制要求开发者处理的异常,这类异常通常是程序可以预见的、外部因素导致的错误,比如读取文件时文件不存在、网络请求时连接超时等。如果方法内部抛出了受检异常,要么在方法上声明throws抛出该异常,要么在方法内部使用try-catch块捕获处理,否则代码无法通过编译。
常见的受检异常包括IOException、SQLException、ClassNotFoundException等,这类异常的设计初衷是提醒调用方需要关注可能出现的异常情况,提前做好应对准备。
非受检异常RuntimeException
非受检异常是编译器不强制要求处理的异常,通常是程序逻辑错误导致的,比如空指针访问、数组越界、除数为0等。这类异常一般是开发者编码时的疏忽造成的,理论上可以通过完善代码逻辑避免。
常见的非受检异常包括NullPointerException、IndexOutOfBoundsException、IllegalArgumentException等,调用方可以选择处理也可以不处理,不过如果放任不管,程序运行到对应逻辑时会直接抛出错误并终止执行。
选择异常类的核心标准
判断异常是否可恢复
这是选择异常类最核心的判断依据。如果异常发生后,调用方可以通过重试、调整参数等方式让程序恢复正常执行,那么优先选择受检异常。比如调用外部接口获取数据时,出现网络超时异常,调用方可以稍后重试请求,这种情况适合用受检异常提醒调用方处理。
如果异常是程序逻辑错误导致的,发生后无法通过外部调整恢复,那么应该选择非受检异常。比如方法要求传入的参数不能为空,调用方传入了null,这种情况属于调用方的逻辑错误,抛出NullPointerException即可,不需要强制调用方处理。
判断是否是调用方的责任
如果异常的产生是因为调用方没有遵守方法的约定,比如传入了不符合要求的参数、没有提前做好前置校验,那么应该抛出非受检异常,提醒调用方修正自身的调用逻辑。比如下面的方法要求传入的年龄必须大于0,否则抛出非法参数异常:
public void setAge(int age) {
if (age <= 0) {
// 调用方传入非法参数,属于逻辑错误,抛出非受检异常
throw new IllegalArgumentException("年龄必须大于0");
}
this.age = age;
}
如果异常的产生不是调用方的责任,而是方法内部依赖的外部资源出现问题,比如读取文件时文件被删除、连接数据库时数据库服务宕机,那么应该抛出受检异常,让调用方知晓可能出现的外部风险。
避免过度使用受检异常
受检异常虽然能强制调用方处理异常,但如果滥用会导致代码冗余。如果方法抛出的受检异常调用方根本无法处理,或者99%的调用方都会选择直接向上抛出,那么不如使用非受检异常。比如下面的场景,底层方法抛出受检异常,上层调用方只是简单向上传递:
// 底层方法抛出受检异常
public void readFile(String path) throws IOException {
// 读取文件逻辑
}
// 上层调用方只是向上抛出,没有实际处理
public void process() throws IOException {
readFile("test.txt");
}
这种情况使用受检异常只会增加代码负担,改成非受检异常会更合适。
自定义异常类的选择
当JDK内置的异常类无法满足业务需求时,开发者可以自定义异常类。自定义异常类时同样要遵循上述选择标准:
- 如果自定义异常是可恢复的、需要调用方主动处理的,就继承Exception,作为受检异常使用。
- 如果自定义异常是逻辑错误导致的、不需要强制调用方处理的,就继承RuntimeException,作为非受检异常使用。
下面是一个自定义受检异常的示例,用于表示业务中的库存不足异常,调用方收到该异常后可以提示用户补货或者调整购买数量:
// 自定义受检异常,继承Exception
public class StockNotEnoughException extends Exception {
private int remainStock;
public StockNotEnoughException(String message, int remainStock) {
super(message);
this.remainStock = remainStock;
}
public int getRemainStock() {
return remainStock;
}
}
再来看一个自定义非受检异常,用于表示订单状态不合法的逻辑错误,属于调用方没有遵守订单状态流转规则导致的:
// 自定义非受检异常,继承RuntimeException
public class InvalidOrderStatusException extends RuntimeException {
public InvalidOrderStatusException(String message) {
super(message);
}
}
常见选择误区
很多开发者会陷入两个极端,要么所有异常都用受检异常,要么所有异常都用RuntimeException。前者会导致代码充斥着大量无意义的try-catch或者throws声明,降低代码可读性;后者会让调用方无法提前知晓可能出现的外部风险,增加程序运行时的不确定性。
还有开发者会把业务异常全部定义为受检异常,实际上很多业务异常是调用方参数错误导致的,比如用户传入的订单号不存在,这种情况属于调用方的逻辑问题,用非受检异常更合适。只有那些需要调用方根据异常信息做业务处理的场景,比如库存不足、余额不足,才适合用受检异常。
选择异常类的核心原则是:让异常信息准确反映问题本质,同时不给调用方增加不必要的负担。优先从JDK内置异常类中选择,内置类无法满足需求时再自定义,自定义时严格遵循可恢复性、责任归属的判断标准即可。
Java异常异常类选择RuntimeExceptionChecked_Exception异常处理修改时间:2026-07-09 06:51:33