Skip to content

异常处理常用写法 ​

学习目标 ​

  • 能用 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 等假数据掩盖错误。

java
// 语义:只在当前层知道如何恢复时捕获。
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 前面会导致后面的分支不可达。

java
// 语义:子类必须写在父类之前。
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 比强行合并更清楚。

java
// 语义:多重 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 用于把不满足前置条件的调用立即变成明确异常;异常消息应包含能帮助定位的输入或业务字段。

java
// 语义: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。

java
// 语义: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,资源会按声明顺序的逆序关闭。

java
// 语义:需要确保实现 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
    }
}

声明多个资源并按逆序关闭 ​

先声明的资源最后关闭,后声明的资源先关闭;嵌套资源有依赖关系时,把被依赖资源放在前面声明。

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 中抛出无上下文的新异常或返回值覆盖原故障。

java
// 语义: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 适合值域不合法;让异常类型和调用者可修复的问题对应。

java
// 语义: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 中的异常或返回值,只在阅读旧代码时识别,不要在新代码中使用。

java
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,而不是只打印主异常消息。

java
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 构造器。

java
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,后续不要再使用已关闭资源。

java
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
        }
    }
}

简单案例 ​

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 会覆盖原异常或返回值;清理代码不要改变控制流。

课后小问 ​

  1. 为什么 try-with-resources 中主体异常仍能看到 close 异常? 答案:主体异常作为主异常保留,关闭阶段的异常会加入 suppressed 列表。 解析:这样调用方既能按主故障处理,又能通过 getSuppressed() 找到资源释放问题,不会丢失诊断线索。

  2. 为什么不能把 catch (Exception) 放在具体 catch 前面? 答案:Exception 已经能匹配具体异常,后面的具体分支永远无法到达。 解析:Java 编译器会把这种分支判为不可达;先写子类再写父类还能让不同恢复策略保持清晰。

本节小结 ​

  • 当前层只捕获自己能恢复的问题,其他异常应传播或翻译并保留 cause。
  • throw、throws、finally 分别负责抛出、声明和清理,职责不能混用。
  • try-with-resources 自动逆序关闭 AutoCloseable,关闭异常通过 suppressed 保留。
  • 参数校验应选择匹配语义的异常类型,并提供能定位问题的消息。

快速回顾 ​

  • 能写出具体到一般的 catch 顺序。
  • 能判断什么时候用 throw、什么时候用 throws。
  • 能写出单资源和多资源 try-with-resources。
  • 能解释 finally return 和 suppressed exception 的边界。