异常体系与分类

面试官问:"Java 的异常体系结构是什么?"

候选人小吴答:"有 Exception 和 Error,Exception 又分为受检异常和非受检异常。"

面试官点点头:"受检异常和非受检异常有什么区别?"

小吴:"受检异常必须显式处理,非受检异常不用。"

面试官追问:"为什么要这样设计?受检异常有什么缺点?"

小吴停顿了很久,说不出来。

【面试官心理】 这道题考的不是记忆力,是对 Java 语言设计哲学的理解。能说出受检异常设计争议的候选人,是真正思考过这个问题的,直接加分。

一、异常体系结构 🔴

Throwable
├── Error(严重错误,通常不处理)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── VirtualMachineError
└── Exception(可恢复的异常)
    ├── RuntimeException(非受检异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ClassCastException
    │   ├── IllegalArgumentException
    │   └── ArithmeticException
    └── 其他 Exception(受检异常)
        ├── IOException
        ├── SQLException
        └── ClassNotFoundException

二、Error vs Exception 🔴

维度ErrorException
严重程度JVM 级别的严重错误程序可处理的异常情况
可恢复性通常不可恢复大多数可以捕获处理
是否需要处理一般不处理(也处理不了)需要处理
典型例子OOM、StackOverflowIOException、NPE
// Error 的典型场景
try {
    // 递归导致 StackOverflowError
    stackOverflow();
} catch (StackOverflowError e) {
    // 捕获了也没什么意义,栈已经满了
    // 而且捕获本身可能再次触发 StackOverflowError
}
⚠️

不要捕获 ErrorThrowable(除非是框架层面的 catch-all 处理)。Error 发生时 JVM 状态已经不可信,捕获后继续运行可能产生更严重的问题。

三、受检异常 vs 非受检异常 🔴

3.1 定义

受检异常(Checked Exception):继承 Exception 但不继承 RuntimeException,编译器强制要求处理(try-catchthrows 声明)。

非受检异常(Unchecked Exception):继承 RuntimeException,编译器不强制要求处理。

// 受检异常:必须处理,否则编译失败
void readFile(String path) throws IOException { // 方式一:声明抛出
    FileReader fr = new FileReader(path);
}

void readFile2(String path) {
    try { // 方式二:try-catch
        FileReader fr = new FileReader(path);
    } catch (IOException e) {
        // 处理异常
    }
}

// 非受检异常:不处理也能编译通过
void divide(int a, int b) {
    int result = a / b; // 可能抛出 ArithmeticException,但不用显式处理
}

3.2 受检异常的设计争议

支持受检异常的观点

  • 强制调用方意识到异常情况,提高代码健壮性
  • 接口契约的一部分:调用方知道需要处理什么异常

反对受检异常的观点(更多人支持):

  • 代码冗余:很多场景只能向上抛,产生大量样板代码
  • 封装泄漏:接口强制依赖实现细节(如底层用了 JDBC 就要在接口 throws SQLException)
  • 链式传播:异常需要一层层显式 throws,修改底层实现会影响所有上层
// 受检异常的痛点:样板代码
void service() throws ServiceException {
    try {
        repository.query();
    } catch (SQLException e) {
        throw new ServiceException(e); // 只能包一层再抛
    }
}

void repository() throws SQLException {
    try {
        connection.executeQuery();
    } catch (IOException e) {
        throw new SQLException(e);
    }
}

Kotlin、C#、Swift 等现代语言都选择不支持受检异常,说明受检异常在实践中争议较大。

💡

能说出受检异常的设计争议,以及现代语言趋势的候选人,面试官会认为有语言设计思考能力,P6/P7 加分点。

四、异常处理最佳实践 🔴

4.1 不要吞掉异常

// ❌ 最糟糕的写法:异常被吞掉,问题无声无息地消失
try {
    doSomething();
} catch (Exception e) {
    // 什么都不做,或者只打印一行日志
}

// ✅ 至少记录完整的异常信息
try {
    doSomething();
} catch (Exception e) {
    log.error("Failed to do something: {}", e.getMessage(), e); // 带上 e 打印完整堆栈
    throw e; // 或者重新抛出
}

4.2 不要捕获 Exception/Throwable

// ❌ 过于宽泛:会捕获所有异常,包括不应该被处理的
try {
    doSomething();
} catch (Exception e) { // 太宽了
    handleError(e);
}

// ✅ 捕获具体的异常类型
try {
    doSomething();
} catch (IOException e) {
    handleIOError(e);
} catch (IllegalArgumentException e) {
    handleInputError(e);
}

4.3 异常链不要丢失

// ❌ 丢失原始异常
try {
    connection.query(sql);
} catch (SQLException e) {
    throw new ServiceException("Query failed"); // 原始 cause 丢失!
}

// ✅ 保留原始异常作为 cause
try {
    connection.query(sql);
} catch (SQLException e) {
    throw new ServiceException("Query failed", e); // 传入 cause
}

4.4 finally 块中不要抛出异常

// ❌ finally 中抛异常会覆盖 try 中的原始异常
try {
    doSomething(); // 抛出 BusinessException
} finally {
    cleanup(); // 如果这里也抛出异常,BusinessException 就丢失了!
}

// ✅ finally 中的异常要处理掉
try {
    doSomething();
} finally {
    try {
        cleanup();
    } catch (Exception e) {
        log.warn("Cleanup failed", e); // 记录,但不再抛出
    }
}

五、自定义异常规范

// 业务异常基类
public class BusinessException extends RuntimeException {
    private final String code; // 错误码

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }

    public String getCode() { return code; }
}

// 具体业务异常
public class UserNotFoundException extends BusinessException {
    public UserNotFoundException(long userId) {
        super("USER_NOT_FOUND", "User not found: " + userId);
    }
}
💡

生产中推荐:业务异常继承 RuntimeException(非受检),统一错误码,全局 ExceptionHandler 处理。能说出这套最佳实践的候选人,面试官会认为有工程经验。

六、追问升级

面试官:"try-catch 有性能开销吗?"

回答:正常执行路径(没有异常抛出)几乎没有性能开销。只有异常真正被抛出时,才需要收集堆栈信息,这个开销较大(涉及遍历调用栈)。

所以:不要用异常做流程控制,比如用异常判断 key 是否存在。

// ❌ 用异常做流程控制,性能差
try {
    map.get(key).doSomething();
} catch (NullPointerException e) {
    // key 不存在
}

// ✅ 正常条件判断
if (map.containsKey(key)) {
    map.get(key).doSomething();
}

【面试官心理】 问"异常的性能开销",是在测候选人有没有生产意识。用异常做流程控制是初级程序员的常见错误,能说出这个问题并给出正确姿势的候选人,基本确认有一定的工程积累。