checked 与 unchecked 异常区别

面试官问:"Java 为什么要有受检异常?"

候选人小孙答:"为了让开发者必须处理可能发生的异常情况。"

面试官追问:"那为什么 RuntimeException 不用强制处理?"

小孙说:"因为 RuntimeException 通常是编程错误,不是运行时环境问题。"

面试官又问:"那你认为受检异常的设计是好设计吗?实际项目中你会怎么用?"

小孙答不上来。

【面试官心理】 这道题考的是候选人对 Java 语言设计的批判性思考。能说出受检异常优缺点并给出工程建议的,直接拉开 P6 和 P7 的差距。

一、核心区别 🔴

维度受检异常(Checked)非受检异常(Unchecked)
父类Exception 的非 RuntimeException 子类RuntimeException 及其子类
编译器行为强制处理(catch 或 throws)不强制,编译通过
设计意图期望调用方恢复/处理编程错误或不可恢复的情况
典型场景IOException、SQLExceptionNPE、ClassCastException、IllegalArgumentException
处理方式恢复(重试、回退)记录日志、上抛、转换为业务异常

二、受检异常的哲学依据 🔴

受检异常的设计基于"恢复导向"哲学:

// 受检异常的场景:调用方应该能够处理并恢复
void connectToDatabase() throws SQLException {
    // 可能连接失败,但调用方可以:重试、降级、切换数据源
    Connection conn = DriverManager.getConnection(url, user, password);
}

调用方有明确的处理路径:重试、降级到备用数据库等。

非受检异常的场景:调用方无法恢复

void processOrder(Order order) {
    if (order == null) {
        // 调用方传了 null,编程错误,应该让调用方修复代码
        throw new IllegalArgumentException("Order cannot be null");
    }
}

调用方(processOrder)无法"恢复"调用者的编程错误,只能向上抛,让问题尽早暴露。

三、受检异常的工程争议 🟡

3.1 优点

  1. 强制意识:调用方必须面对异常情况,减少遗漏
  2. 接口契约throws 声明是接口契约的一部分,调用方清楚需要处理什么

3.2 缺点

// 问题一:样板代码
void highLevel() throws LowLevelException {
    try {
        mediumLevel();
    } catch (LowLevelException e) {
        throw e; // 必须显式声明抛出
    }
}

// 问题二:异常泄漏污染
interface Repository {
    User findById(long id) throws SQLException, // JDBC 实现需要
                                  IOException;   // IO 实现需要
}
// 接口被实现细节污染了

3.3 工程建议

现代 Java 项目中,受检异常的实际使用策略:

  1. 业务层统一转换为非受检异常:底层抛受检异常,上层统一转换为 BusinessException(RuntimeException)
  2. 少用受检异常:除非异常确实需要调用方实际恢复,否则优先使用非受检异常
  3. Spring 框架的选择Spring 大量使用非受检异常(DataAccessExceptionRuntimeException 的子类)
// 现代最佳实践:统一转换
try {
    jdbcTemplate.query(sql);
} catch (DataAccessException e) { // Spring 的非受检异常
    throw new BusinessException("DB_ERROR", "Query failed", e);
}
💡

Spring、Hibernate 等主流框架都选择用非受检异常包装受检异常,说明工业界更认可"非受检优先"的策略。能说出这一点的候选人,面试官会认为有实际项目经验。

四、RuntimeException 的分类 🟡

RuntimeException 并非都是"编程错误",分为两类:

// 第一类:编程错误(应该修复代码)
NullPointerException    // 调用了 null 对象的方法
ArrayIndexOutOfBoundsException // 数组越界
ClassCastException      // 强制类型转换失败
IllegalArgumentException // 参数不合法
ArithmeticException     // 算术错误(如除零)

// 第二类:环境/资源错误(可能发生但应该让调用方知道)
OutOfMemoryError        // 虽然是 Error,但性质类似
StackOverflowError      // 同上
SecurityException       // 安全策略限制

五、追问升级

面试官:"如果一个方法既可能抛出受检异常,又可能抛出非受检异常,应该如何声明?"

// 方法一:分开 throws(推荐)
void process() throws IOException, // 受检
                          NullPointerException { // 可以不声明,因为是unchecked

}

// 方法二:统一 throws Exception
void process() throws Exception {
    // 所有异常都抛给调用方
}

推荐方式一:显式声明受检异常,不声明非受检异常(因为编译器不要求,且调用方通常无法处理)。

面试官:"catch 块中先 catch 受检异常还是非受检异常?"

try {
    doSomething();
} catch (IOException e) {  // ✅ 先 catch 更具体的
    handleIOException(e);
} catch (Exception e) {     // 最后 catch 更宽泛的
    handleGeneric(e);
}

必须先 catch 具体异常,再 catch 宽泛异常,否则编译报错("unreachable catch block")。

【面试官心理】 问 catch 顺序,是在测候选人对异常处理细节的掌握程度。答错顺序的候选人,说明从来没有在 IDE 之外写过异常处理代码。