异常处理常用写法
学习目标
- 能用 try-catch 处理可恢复错误,并保持捕获顺序正确。
- 分清 throw、throws、finally 和 try-with-resources 的职责。
- 在异常处理边界保留上下文,不吞掉真正需要诊断的根因。
核心知识点
专业术语
- Throwable:所有错误和异常的根类型。
- checked exception:除 RuntimeException 和 Error 分支外,编译器要求捕获或声明的异常。
- unchecked exception:RuntimeException 及 Error 分支,不要求在签名中声明。
- 异常处理边界:决定当前层恢复、翻译、记录还是继续向上抛出的地方。
- try-with-resources:自动调用 AutoCloseable.close() 的资源管理语法。
白话解释与边界
异常不是万能返回值。能在当前层恢复的输入问题可以捕获并给出结果;无法恢复或需要上层决定的错误应继续传播;翻译异常时保留 cause。不要写一个 catch (Exception) 就把所有问题变成“失败”,也不要 catch 后什么都不做。
throw 是现在抛出异常对象,throws 是方法契约声明可能传播的异常。finally 适合释放无法自动管理的资源,但不要在 finally 中 return,因为它会覆盖 try 或 catch 的返回值和异常。
常用用法
用 try-catch 处理可恢复输入错误
只在当前层知道如何恢复时捕获;如果无法给出合理默认值,不要用 0 等假数据掩盖错误。
// 语义:只在当前层知道如何恢复时捕获。
public class TryCatchDemo {
static int parseAge(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException error) {
return 0;
}
}
public static void main(String[] args) {
System.out.println(parseAge("unknown"));
// 输出:0
}
}按具体到一般的顺序捕获异常
子类必须写在父类之前;把 RuntimeException 放在 IllegalArgumentException 前面会导致后面的分支不可达。
// 语义:子类必须写在父类之前。
public class CatchOrderDemo {
static void run() {
try {
throw new IllegalArgumentException("bad input");
// 异常:throw new IllegalArgumentException("bad input") 立即进入异常路径。
// 初始状态:表达式为 throw new IllegalArgumentException("bad input")。
} catch (IllegalArgumentException error) {
System.out.println("input=" + error.getMessage());
// 输出:input=bad input
} catch (RuntimeException error) {
System.out.println("runtime");
// 输出:runtime
}
}
public static void main(String[] args) {
run();
}
}用多重 catch 合并相同处理
多重 catch 中的异常类型不能存在父子关系;如果处理逻辑不同,拆成多个 catch 比强行合并更清楚。
// 语义:多重 catch 中的异常类型不能存在父子关系。
public class MultiCatchDemo {
static String normalize(String text) {
try {
return text.trim().substring(0, 3);
} catch (NullPointerException | StringIndexOutOfBoundsException error) {
return "invalid";
}
}
public static void main(String[] args) {
System.out.println(normalize("a"));
// 输出:invalid
}
}用 throw 主动拒绝非法状态
throw 用于把不满足前置条件的调用立即变成明确异常;异常消息应包含能帮助定位的输入或业务字段。
// 语义:throw 用于把不满足前置条件的调用立即变成明确异常。
public class ThrowDemo {
static int percentage(int value) {
if (value < 0 || value > 100) {
throw new IllegalArgumentException("value must be 0..100");
// 异常:throw new IllegalArgumentException("value must be 0..100") 立即进入异常路径。
// 初始状态:表达式为 throw new IllegalArgumentException("value must be 0..100")。
}
return value;
}
public static void main(String[] args) {
System.out.println(percentage(80));
// 输出:80
}
}用 throws 声明受检异常
throws 是方法契约,不会自动处理异常;调用方必须捕获或继续声明 checked exception。
// 语义:throws 是方法契约,不会自动处理异常。
import java.io.IOException;
public class ThrowsDemo {
static String readConfig() throws IOException {
throw new IOException("config is unavailable");
// 异常:throw new IOException("config is unavailable") 立即进入异常路径。
// 初始状态:表达式为 throw new IOException("config is unavailable")。
}
public static void main(String[] args) {
try {
readConfig();
} catch (IOException error) {
System.out.println(error.getMessage());
// 输出:config is unavailable
}
}
}用 try-with-resources 自动关闭资源
需要确保实现 AutoCloseable 的资源在异常路径也能关闭时使用 try-with-resources,资源会按声明顺序的逆序关闭。
// 语义:需要确保实现 AutoCloseable 的资源在异常路径也能关闭时使用 try-with-resources,资源会按声明顺序的逆序关闭。
import java.io.StringReader;
import java.io.BufferedReader;
import java.io.IOException;
public class TryWithResourcesDemo {
static String firstLine() throws IOException {
try (BufferedReader reader =
new BufferedReader(new StringReader("java\napi"))) {
// 初始状态:new StringReader("java\napi") 提供两行文本,BufferedReader reader 包装它;try 结束时 reader 自动关闭。
return reader.readLine();
}
}
public static void main(String[] args) throws IOException {
System.out.println(firstLine());
// 输出:java
}
}声明多个资源并按逆序关闭
先声明的资源最后关闭,后声明的资源先关闭;嵌套资源有依赖关系时,把被依赖资源放在前面声明。
// 语义:先声明的资源最后关闭,后声明的资源先关闭。
public class ResourceOrderDemo {
static final class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
}
@Override
public void close() {
System.out.println("close " + name);
// 输出:close second,然后 close first(second 后声明所以先关闭)。
}
}
public static void main(String[] args) {
try (Resource first = new Resource("first");
// 关键变化:first 接收表达式 new Resource("first") 的计算结果。
// 初始状态:first 的初始值为 new Resource("first")。
Resource second = new Resource("second")) {
// 初始状态:first 和 second 分别包装资源名 "first"、"second",退出 try 时按逆序关闭。
System.out.println("work");
// 输出:work
}
}
}用 finally 做不能遗漏的清理
finally 通常用于没有 AutoCloseable 接口的清理动作;不要在 finally 中抛出无上下文的新异常或返回值覆盖原故障。
// 语义:finally 通常用于没有 AutoCloseable 接口的清理动作。
public class FinallyDemo {
static boolean locked;
static void work() {
locked = true;
try {
System.out.println("work");
// 输出:work
} finally {
locked = false;
}
}
public static void main(String[] args) {
work();
System.out.println(locked);
// 输出:false
}
}先校验再抛出明确的参数异常
Objects.requireNonNull 适合 null 前置条件,IllegalArgumentException 适合值域不合法;让异常类型和调用者可修复的问题对应。
// 语义:Objects.requireNonNull 适合 null 前置条件,IllegalArgumentException 适合值域不合法。
// 初始状态:id 初始为 Objects.requireNonNull(value, "id")。
import java.util.Objects;
public class ArgumentValidationDemo {
static String userId(String value) {
String id = Objects.requireNonNull(value, "id");
// 关键变化:id 接收表达式 Objects.requireNonNull(value, "id") 的计算结果。
// 初始状态:id 的初始值为 Objects.requireNonNull(value, "id")。
if (id.isBlank()) {
throw new IllegalArgumentException("id must not be blank");
}
return id;
}
public static void main(String[] args) {
System.out.println(userId("u-1"));
// 输出:u-1
}
}不常用但需要知道
了解 finally 中 return 会覆盖原结果
这类写法会吞掉 try 中的异常或返回值,只在阅读旧代码时识别,不要在新代码中使用。
public class FinallyReturnDemo {
static int bad() {
try {
return 1;
} finally {
return 2;
}
}
public static void main(String[] args) {
System.out.println(bad());
// 输出:2
}
}用 try-with-resources 观察 suppressed exception
主体异常会保留为主异常,关闭异常通过 getSuppressed() 附加;日志或诊断代码应检查 suppressed,而不是只打印主异常消息。
public class SuppressedDemo {
static final class BrokenResource implements AutoCloseable {
@Override
public void close() {
throw new IllegalStateException("close failed");
// 异常:throw new IllegalStateException("close failed") 立即进入异常路径。
// 初始状态:表达式为 throw new IllegalStateException("close failed")。
}
}
public static void main(String[] args) {
try (BrokenResource resource = new BrokenResource()) {
// 初始状态:resource 可关闭但 close() 会抛出 message="close failed" 的异常。
throw new IllegalStateException("body failed");
} catch (IllegalStateException error) {
System.out.println(error.getMessage());
// 输出:body failed
System.out.println(error.getSuppressed()[0].getMessage());
// 输出:close failed
}
}
}用 rethrow 保留原异常类型
捕获后做记录或补充上下文,再直接抛出原对象可以保留类型和堆栈;如果要换成领域异常,应使用 cause 构造器。
import java.io.IOException;
public class RethrowDemo {
static void load(boolean missing) throws IOException {
try {
if (missing) {
throw new IOException("missing");
// 异常:throw new IOException("missing") 立即进入异常路径。
// 初始状态:表达式为 throw new IOException("missing")。
}
} catch (IOException error) {
System.out.println("record=" + error.getMessage());
// 输出:record=missing
throw error;
}
}
public static void main(String[] args) {
try {
load(true);
} catch (IOException error) {
System.out.println("caller=" + error.getMessage());
// 输出:caller=missing
}
}
}用资源变量重用已打开的 AutoCloseable
JDK 9 起可以把 effectively final 的资源变量直接放进 try;所有权会转移给 try-with-resources,后续不要再使用已关闭资源。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
public class ExistingResourceDemo {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new StringReader("java"));
// 关键变化:reader 接收表达式 new BufferedReader(new StringReader("java")) 的计算结果。
// 初始状态:reader 的初始值为 new BufferedReader(new StringReader("java"))。
try (reader) {
System.out.println(reader.readLine());
// 输出:java
}
}
}简单案例
public class ExceptionSummaryDemo {
public static void main(String[] args) {
try {
Integer.parseInt("bad");
} catch (NumberFormatException error) {
System.out.println("invalid");
// 输出:invalid
}
}
}最小异常处理只在当前层知道如何恢复时捕获;否则应带上下文继续传播。
易混点
- throw 是实际抛出,throws 是声明传播;只写 throws 不代表已经处理。
- catch 顺序必须从具体到一般;多重 catch 的类型不能有继承关系。
- try-with-resources 关闭顺序是声明顺序的逆序,主体异常通常优先于关闭异常。
- finally 中的 return 会覆盖原异常或返回值;清理代码不要改变控制流。
课后小问
为什么 try-with-resources 中主体异常仍能看到 close 异常? 答案:主体异常作为主异常保留,关闭阶段的异常会加入 suppressed 列表。 解析:这样调用方既能按主故障处理,又能通过 getSuppressed() 找到资源释放问题,不会丢失诊断线索。
为什么不能把 catch (Exception) 放在具体 catch 前面? 答案:Exception 已经能匹配具体异常,后面的具体分支永远无法到达。 解析:Java 编译器会把这种分支判为不可达;先写子类再写父类还能让不同恢复策略保持清晰。
本节小结
- 当前层只捕获自己能恢复的问题,其他异常应传播或翻译并保留 cause。
- throw、throws、finally 分别负责抛出、声明和清理,职责不能混用。
- try-with-resources 自动逆序关闭 AutoCloseable,关闭异常通过 suppressed 保留。
- 参数校验应选择匹配语义的异常类型,并提供能定位问题的消息。
快速回顾
- 能写出具体到一般的 catch 顺序。
- 能判断什么时候用 throw、什么时候用 throws。
- 能写出单资源和多资源 try-with-resources。
- 能解释 finally return 和 suppressed exception 的边界。
阿源的知识库