checked 与 unchecked 异常区别
面试官问:"Java 为什么要有受检异常?"
候选人小孙答:"为了让开发者必须处理可能发生的异常情况。"
面试官追问:"那为什么 RuntimeException 不用强制处理?"
小孙说:"因为 RuntimeException 通常是编程错误,不是运行时环境问题。"
面试官又问:"那你认为受检异常的设计是好设计吗?实际项目中你会怎么用?"
小孙答不上来。
【面试官心理】
这道题考的是候选人对 Java 语言设计的批判性思考。能说出受检异常优缺点并给出工程建议的,直接拉开 P6 和 P7 的差距。
一、核心区别 🔴
二、受检异常的哲学依据 🔴
受检异常的设计基于"恢复导向"哲学:
// 受检异常的场景:调用方应该能够处理并恢复
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 优点
- 强制意识:调用方必须面对异常情况,减少遗漏
- 接口契约:
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 项目中,受检异常的实际使用策略:
- 业务层统一转换为非受检异常:底层抛受检异常,上层统一转换为
BusinessException(RuntimeException)
- 少用受检异常:除非异常确实需要调用方实际恢复,否则优先使用非受检异常
- Spring 框架的选择:
Spring 大量使用非受检异常(DataAccessException 是 RuntimeException 的子类)
// 现代最佳实践:统一转换
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 之外写过异常处理代码。