Java 异常处理

要点

异常把失败沿调用链传播。checked 异常要求声明或捕获;unchecked 不免除处理责任,只改变编译期要求。

异常体系回顾

Throwable
├── Error                    # JVM 级严重错误,不应捕获(OutOfMemoryError、StackOverflowError)
└── Exception
    ├── RuntimeException     # 非受检:NPE、IndexOutOfBounds、IllegalArgument、IllegalState...
    └── 其他 Exception       # 受检:IOException、SQLException、TimeoutException...

受检/非受检的决策口诀:调用方能合理恢复 → 受检;编程缺陷或无法恢复 → 非受检(RuntimeException)。现代 Java 生态(Spring 等)总体偏向”少用受检异常 + 声明式兜底”。

try-with-resources(Java 7+)

旧写法:finally 中判空、逐个 close、还要提防 close 抛出的异常掩盖业务异常。

现代写法:

try (var reader = Files.newBufferedReader(path);
     var writer = Files.newBufferedWriter(target)) {
    writer.write(reader.readLine());
}   // 自动按声明逆序关闭;close 抛的异常被"抑制",不会覆盖业务异常
  • 任何实现 AutoCloseable 的对象都能用
  • 资源变量可以是 try 外的”事实最终变量”(Java 9+):
var conn = openConnection();
try (conn) {            // 直接引用已有资源
    conn.query(sql);
}

可完整编译和运行的验证示例(Java 21)

下面这段程序把正文中的两个关键承诺放在一个可观察的场景里:业务异常仍是主异常,两个资源按声明的逆序关闭,close() 的失败出现在主异常的 getSuppressed() 中。

public class ExceptionResourcesVerification {
    static final class Resource implements AutoCloseable {
        private final String name;
        Resource(String name) { this.name = name; }
        @Override public void close() { throw new IllegalStateException("close:" + name); }
    }

    public static void main(String[] args) {
        try (var first = new Resource("first"); var second = new Resource("second")) {
            throw new IllegalArgumentException("work");
        } catch (IllegalArgumentException error) {
            System.out.println(error.getMessage());
            for (Throwable suppressed : error.getSuppressed()) {
                System.out.println(suppressed.getMessage());
            }
        }
    }
}

多捕获与精确重抛

try {
    importUsers(input);
} catch (IOException | DateTimeParseException e) {   // 多类型捕获,e 隐式 final
    throw new ImportException("导入失败: " + input, e);   // 包装时必须保留原因链!
}
  • 包装异常时永远传入 cause,否则堆栈断链,线上排查痛苦加倍
  • getCause() / getSuppressed() 是两个常被忽略的排查入口
  • 自定义异常建议提供 (String message) 与 (String message, Throwable cause) 双构造器

现代异常设计原则

  1. 类型说”出了什么事”,消息说”具体是什么事” new IllegalArgumentException("age 不能为负: " + age) 优于裸的 new IllegalArgumentException("error")
  2. 不用异常做流程控制——异常构造含堆栈快照,成本高且误导调用方
  3. 预期缺失用 Optional,意外失败用异常

java // Optional 表达"可能没有",让调用方显式决策 User user = repo.findById(id) .orElseThrow(() -> new UserNotFoundException(id));

  1. 自定义业务异常默认继承 RuntimeException——Spring 声明式事务默认只回滚运行时异常(详见 JPA 核心)
  2. catch 块只做三件事之一:恢复、记录、重新抛出(可包装)
// ❌ 吞异常:出了问题世界静默
try { loadConfig(); } catch (IOException e) { }

// ✅ 记录或包装,二选一
try { loadConfig(); } catch (IOException e) {
    throw new UncheckedIOException("读取配置失败: " + configPath, e);
}

异常与 Stream/Optional 的配合

  • Stream 里调到受检异常:抽一个小方法包装为非受检异常,保持管道整洁
  • orElseThrow(Supplier) 是比 get() 更安全的取值方式:
// Stream 场景:找不到即快速失败
Book book = books.stream()
        .filter(b -> b.isbn().equals(isbn))
        .findFirst()
        .orElseThrow(() -> new BookNotFoundException(isbn));

最佳实践 / 陷阱清单

捕获异常后要决定恢复、转换还是传播,并在一个合适边界记录原因。InterruptedException 如果继续向上抛出就不必再原地恢复;若无法传播且终止本次工作,通常恢复中断标记让上层知道取消请求。

使用 try-with-resources 关闭支持该协议的资源,finally 中不要返回新结果覆盖原异常。宽泛捕获可以用于服务边界兜底,但不能吞掉未知错误继续假装成功;底层日志与用户消息分开。

练习与实践

练习一:资源管理迁移

  1. 写一段 finally-close 的旧代码,迁移为 try-with-resources
  2. 在 close 中故意抛异常,观察”抑制异常”行为(getSuppressed())

练习二:异常体系设计

  1. 为图书管理场景设计 BookNotFoundException/DuplicateIsbnException
  2. 保证两处都有完整原因链与有信息量的消息

练习三:Optional 快速失败

  1. 用 Stream + orElseThrow 重写一段”判空 if + 手动 throw”的旧逻辑

相关文档

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇