Wrayの知识库 Wrayの知识库
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
  • Java章节编写规范
  • Java基础

    • Java概述
    • Java语法
    • 面向对象编程
    • Java数组
    • String字符串
    • 异常处理
      • 1. 异常体系
      • 2. Checked Exception 与 Unchecked Exception
      • 3. 异常传播流程
      • 4. try-catch
      • 5. finally
      • 6. try-with-resources
        • 6.1 被抑制异常
      • 7. throw 与 throws
      • 8. 自定义异常
      • 9. 日志与异常
      • 10. 异常与事务
      • 11. 异常性能
      • 12. 常见异常场景
      • 13. 异常处理原则
        • 13.1 能处理才捕获
        • 13.2 不要吞异常
        • 13.3 不要直接捕获 Throwable
        • 13.4 异常信息要可行动
        • 13.5 不暴露敏感信息
      • Tips 快问快答
    • 基本类型与包装类
    • Object类与常用方法
    • 泛型
    • 枚举与注解
    • 反射
    • 值传递与引用
    • Lambda表达式与函数式接口
    • Stream API
    • Optional与空值处理
    • 日期时间API
    • Java现代语法特性
  • Java集合

  • Java并发

  • Java IO

  • JVM

  • Java
  • Java基础
Wray
2026-06-24
目录

异常处理

异常处理是 Java 程序可靠性的基础。它负责把程序运行中的错误从正常业务流程中分离出来,让调用方有机会感知、恢复、记录或终止流程。

新手常把异常当成“报错信息”,高级开发者会把异常看成一种调用契约:方法失败时如何表达失败原因,调用方应该如何处理,日志应该记录到哪里,资源应该如何释放。

# 1. 异常体系

Java 中所有异常和错误都继承自 Throwable。

Throwable
├─ Error
│  ├─ OutOfMemoryError
│  ├─ StackOverflowError
│  └─ ...
│
└─ Exception
   ├─ IOException
   ├─ SQLException
   ├─ ReflectiveOperationException
   └─ RuntimeException
      ├─ NullPointerException
      ├─ IllegalArgumentException
      ├─ IllegalStateException
      ├─ IndexOutOfBoundsException
      └─ ...

分类说明:

类型 编译器是否强制处理 典型原因 常见处理方式
Error 否 JVM 或系统级严重问题 通常不捕获,排查系统问题
受检异常 是 外部资源失败,如 IO、数据库 捕获处理或继续声明抛出
非受检异常 否 程序逻辑错误、参数错误、状态错误 修正代码或在边界统一处理

# 2. Checked Exception 与 Unchecked Exception

受检异常是 Exception 的子类,但不是 RuntimeException 的子类。

public String read(String path) throws IOException {
    return Files.readString(Path.of(path));
}

调用方必须处理:

try {
    String content = read("a.txt");
} catch (IOException e) {
    log.error("读取文件失败", e);
}

非受检异常继承自 RuntimeException:

public void updateAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年龄不能为负数");
    }
}

选择建议:

场景 建议
调用方有明确恢复策略 可以使用受检异常
参数非法、状态非法、编程错误 使用非受检异常
业务规则失败 常用自定义非受检业务异常
框架底层异常包装 常转换为统一运行时异常

很多企业项目倾向于使用运行时业务异常,并在 Web 层统一转换成错误响应。

# 3. 异常传播流程

当方法内部抛出异常时,如果当前方法不处理,异常会沿调用栈向上传播。

public void controller() {
    service();
}

public void service() {
    repository();
}

public void repository() {
    throw new IllegalStateException("数据库状态异常");
}

传播过程:

repository 抛出异常
   │ 当前方法未捕获
   ▼
service 被异常打断
   │ 当前方法未捕获
   ▼
controller 被异常打断
   │ 当前方法未捕获
   ▼
线程默认异常处理器 / 框架异常处理器

异常一旦抛出,当前执行路径会立即中断,后续普通语句不会继续执行。

public void test() {
    System.out.println("A");
    throw new RuntimeException("error");
    // System.out.println("B"); // 不可达
}

# 4. try-catch

基本结构:

try {
    int value = 10 / 0;
} catch (ArithmeticException e) {
    log.error("计算失败", e);
}

多个 catch:

try {
    readFile();
} catch (FileNotFoundException e) {
    log.warn("文件不存在", e);
} catch (IOException e) {
    log.error("读取失败", e);
}

子类异常必须放在父类异常前面:

try {
    readFile();
} catch (IOException e) {
    // ...
} catch (FileNotFoundException e) {
    // 编译失败,前面已经捕获 IOException
}

多异常捕获:

try {
    parse();
} catch (IOException | NumberFormatException e) {
    log.error("解析失败", e);
}

catch 的目标不是“让程序不报错”,而是把失败转换为调用方可理解的结果,或者记录足够信息后继续向上抛。

# 5. finally

finally 通常用于释放资源,无论是否发生异常都会执行。

FileInputStream input = null;
try {
    input = new FileInputStream("a.txt");
    // read
} catch (IOException e) {
    log.error("读取失败", e);
} finally {
    if (input != null) {
        try {
            input.close();
        } catch (IOException e) {
            log.error("关闭失败", e);
        }
    }
}

执行顺序:

try 正常完成
  │
  ▼
finally
  │
  ▼
继续执行

try 抛出异常
  │
  ▼
匹配 catch
  │
  ▼
finally
  │
  ▼
继续或向上抛出

不要在 finally 中写 return:

public int value() {
    try {
        return 1;
    } finally {
        return 2;
    }
}

结果是 2,finally 中的 return 会覆盖 try 中的返回值,也可能吞掉异常。

# 6. try-with-resources

Java 7 引入 try-with-resources,用于自动关闭实现了 AutoCloseable 的资源。

try (BufferedReader reader = Files.newBufferedReader(Path.of("a.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    log.error("读取失败", e);
}

等价思路:

打开资源
  │
  ▼
执行 try 块
  │
  ▼
自动 close
  │
  ▼
如有异常,进入 catch

多个资源按声明顺序打开,按相反顺序关闭:

try (
    InputStream input = Files.newInputStream(source);
    OutputStream output = Files.newOutputStream(target)
) {
    input.transferTo(output);
}

关闭顺序:

打开 input
打开 output
使用资源
关闭 output
关闭 input

# 6.1 被抑制异常

如果 try 块抛出异常,关闭资源时也抛出异常,关闭异常会作为 suppressed exception 挂到主异常上。

catch (IOException e) {
    for (Throwable suppressed : e.getSuppressed()) {
        log.warn("关闭资源时发生异常", suppressed);
    }
}

这比传统 finally 更安全,因为传统写法中 close 异常容易覆盖主异常。

# 7. throw 与 throws

throw 用于真正抛出一个异常对象:

throw new IllegalArgumentException("参数错误");

throws 用于方法签名声明可能抛出的异常:

public void read() throws IOException {
    // ...
}

区别:

关键字 位置 作用
throw 方法体内 抛出异常对象
throws 方法声明上 声明异常可能向外传播

示例:

public void save(User user) {
    if (user == null) {
        throw new IllegalArgumentException("user 不能为空");
    }
}

# 8. 自定义异常

自定义异常可以让错误语义更清晰。

public class BusinessException extends RuntimeException {
    private final String code;

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

    public String getCode() {
        return code;
    }
}

使用:

if (stock < quantity) {
    throw new BusinessException("STOCK_NOT_ENOUGH", "库存不足");
}

设计建议:

建议 原因
异常名表达语义 比 RuntimeException 更容易定位问题
保留 cause 不丢失底层异常堆栈
不要过度细分 太多异常类型会增加维护成本
业务异常带错误码 便于前后端统一处理

包装异常时保留原因:

try {
    remoteCall();
} catch (IOException e) {
    throw new BusinessException("REMOTE_FAILED", "远程调用失败", e);
}

需要给自定义异常增加对应构造器:

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

# 9. 日志与异常

异常日志要包含上下文,但不要重复打印。

推荐:

log.error("创建订单失败, userId={}, skuId={}", userId, skuId, e);

不推荐:

log.error(e.getMessage());

原因是只打印 message 会丢失堆栈。

也不推荐每一层都打印再抛出:

catch (Exception e) {
    log.error("失败", e);
    throw e;
}

如果每层都这样做,同一个异常会被打印多次,干扰排查。

常见分层策略:

Repository 层:捕获底层异常,必要时转换
Service 层:补充业务语义,决定是否回滚
Controller 层:统一异常处理,输出响应
全局异常处理器:记录未处理异常

# 10. 异常与事务

在 Spring 这类框架中,事务回滚通常和异常类型有关。默认情况下,运行时异常和错误会触发回滚,受检异常是否回滚需要额外配置。

常见问题:

@Transactional
public void createOrder() {
    try {
        saveOrder();
        deductStock();
    } catch (Exception e) {
        log.error("创建订单失败", e);
    }
}

这里异常被吞掉,事务框架可能认为方法正常结束,从而提交事务。

更合理的写法:

@Transactional
public void createOrder() {
    try {
        saveOrder();
        deductStock();
    } catch (Exception e) {
        log.error("创建订单失败", e);
        throw e;
    }
}

或者抛出业务异常,让统一异常处理器转换响应。

# 11. 异常性能

创建异常对象通常需要填充堆栈信息,这比普通对象创建更昂贵。

new Exception()
  │
  ▼
采集当前调用栈
  │
  ▼
保存 stack trace

因此不要用异常控制正常流程:

try {
    int value = Integer.parseInt(text);
} catch (NumberFormatException e) {
    // 如果这是高频正常分支,成本会偏高
}

如果输入非法是高频可预期情况,优先在解析前做校验,或者使用返回结果表达失败。

# 12. 常见异常场景

异常 常见原因 处理思路
NullPointerException 空引用调用方法或访问字段 明确 null 语义,入参校验
IllegalArgumentException 方法参数不合法 调用方修正参数
IllegalStateException 当前对象状态不允许操作 检查状态流转
IndexOutOfBoundsException 下标越界 检查边界
ClassCastException 类型强转错误 使用 instanceof 判断
ConcurrentModificationException 遍历集合时结构性修改 使用迭代器删除或并发容器
IOException 文件、网络读写失败 重试、降级、提示用户

# 13. 异常处理原则

# 13.1 能处理才捕获

如果当前层无法恢复,也无法补充有价值的信息,就不要为了“看起来处理了”而捕获。

# 13.2 不要吞异常

不推荐:

try {
    doSomething();
} catch (Exception e) {
}

这会让问题消失在日志之外。

# 13.3 不要直接捕获 Throwable

Throwable 包含 Error,随意捕获可能掩盖严重系统问题。

# 13.4 异常信息要可行动

异常 message 应说明具体失败原因:

throw new IllegalArgumentException("userId 不能为空");

比下面更有价值:

throw new IllegalArgumentException("参数错误");

# 13.5 不暴露敏感信息

对外响应不要直接返回完整异常堆栈、SQL、密钥、用户隐私等信息。内部日志可以保留必要上下文,对外响应应转换为安全的错误码和提示。

# Tips 快问快答

Q:Exception 和 Error 有什么区别? A:Exception 通常表示程序可处理或可预期的异常;Error 通常表示 JVM 或系统级严重问题,一般不捕获。

Q:受检异常和非受检异常怎么区分? A:继承 RuntimeException 的是非受检异常;其他 Exception 子类通常是受检异常。

Q:什么时候用自定义异常? A:当通用异常无法表达业务语义,或者需要携带错误码、领域信息时使用。

Q:为什么不要捕获后什么都不做? A:异常被吞掉后,程序可能继续在错误状态下运行,排查时也没有日志线索。

Q:finally 一定会执行吗? A:通常会,但 JVM 直接退出、进程被杀、机器断电等极端情况不会保证执行。

Q:为什么不建议在 finally 里 return? A:它会覆盖 try 或 catch 的返回值,也可能吞掉原本要抛出的异常。

Q:try-with-resources 关闭资源的顺序是什么? A:多个资源按声明顺序打开,按相反顺序关闭。

Q:异常日志应该在哪一层打印? A:通常在边界层或统一异常处理器打印。中间层如果只是继续抛出,避免重复打印。

Q:Spring 事务里捕获异常会影响回滚吗? A:会。异常被捕获且不再抛出时,事务框架可能认为方法正常结束,从而提交事务。

Q:异常可以用于正常业务分支吗? A:不建议。异常创建和堆栈采集成本较高,也会让正常逻辑难读。

上次更新: 2026/06/24, 16:28:28
String字符串
基本类型与包装类

← String字符串 基本类型与包装类→

Copyright © 2023-2026 Wray | 鄂ICP备2024050235号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式