V
Vel·ToolKit
简洁 · 高效 · 即开即用
ZH
第 11 章 / 共 20 章

异常处理

checked vs unchecked、try-with-resources、异常链、自定义异常

什么时候用异常

异常用于表达"非预期但需要被处理"的情况,典型场景:读文件失败、网络断开、数据库连接超时、用户输入非法、JSON 解析错误。Java 把它们设计成可抛出/可捕获的对象,而不是返回值,调用方可以选择就地处理、向上传递、或者整体兜底。

  • ✅ 合适场景:IO 失败 / 网络错误 / 资源不可用 / 业务校验不通过 / 配置缺失
  • ❌ 不合适场景:用异常控制流程(比如用 NumberFormatException 判断字符串是不是数字)
  • 判断原则:如果"出错"是常态的一部分(如 form 校验),考虑用返回值 / Optional;只有真正"异常"才抛

throw 与 throws

throw 用于在方法内抛出异常对象;throws 写在方法签名上,声明这个方法可能抛出哪些 checked 异常,强制调用者要么 catch、要么继续 throws。

// ThrowDemo.java
public class ThrowDemo {
    static int divide(int a, int b) {
        if (b == 0) {
            throw new ArithmeticException("divide by zero");
        }
        return a / b;
    }

    public static void main(String[] args) {
        System.out.println(divide(10, 2));
        try {
            System.out.println(divide(10, 0));
        } catch (ArithmeticException e) {
            System.out.println("caught: " + e.getMessage());
        }
    }
}

checked vs unchecked

Java 把异常分两类:checked(受检)必须被处理或声明 throws,编译器强制;unchecked(RuntimeException 及其子类、Error)不强制处理。

  • checked:IOException、SQLException、ClassNotFoundException 等——常见于库 API,强制使用者面对
  • unchecked(RuntimeException 家族):NullPointerException、IllegalArgumentException、IllegalStateException、IndexOutOfBoundsException——多半是"程序员错误"
  • Error:OutOfMemoryError、StackOverflowError——JVM 级别故障,不要 catch
// CheckedVsUnchecked.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CheckedVsUnchecked {
    // 必须 throws 或就地处理(checked)
    static String readConfig(String path) throws IOException {
        return Files.readString(Path.of(path));
    }

    // 不需要声明(unchecked)
    static int parseAge(String s) {
        int n = Integer.parseInt(s); // 解析失败抛 NumberFormatException
        if (n < 0) throw new IllegalArgumentException("age < 0");
        return n;
    }

    public static void main(String[] args) throws IOException {
        System.out.println(parseAge("30"));
        try {
            parseAge("abc");
        } catch (NumberFormatException e) {
            System.out.println("bad number: " + e.getMessage());
        }
    }
}

try-catch-finally 与多重 catch

finally 块无论是否抛异常都会执行,常用于清理资源(旧版做法)。多重 catch 用 `|` 合并:多种异常做相同处理。

// TryCatch.java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;

public class TryCatch {
    public static void main(String[] args) {
        try {
            String content = Files.readString(Path.of("/no/such/file"));
            System.out.println(content);
        } catch (NoSuchFileException e) {
            System.out.println("file missing: " + e.getFile());
        } catch (IOException | RuntimeException e) {
            // 多种异常合并处理
            System.out.println("other error: " + e);
        } finally {
            System.out.println("cleanup runs anyway");
        }
    }
}

try-with-resources(IO / DB 必备)

凡是"打开了就必须关闭"的资源——文件流、数据库连接、HTTP client、socket——都应该用 try-with-resources。括号里声明的对象只要实现了 AutoCloseable,离开 try 块时自动 close(),比 try-finally 更短更安全(避免漏关,避免 close 自己抛异常吞掉原异常)。

// TryWithResources.java
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class TryWithResources {
    public static void main(String[] args) throws IOException {
        Path p = Files.createTempFile("demo", ".txt");
        Files.writeString(p, "line1\nline2\nline3");

        // 多个资源用分号分隔,关闭顺序与声明相反
        try (BufferedReader r = Files.newBufferedReader(p)) {
            String line;
            while ((line = r.readLine()) != null) {
                System.out.println(line);
            }
        }
        // 走到这里时 r 已经被自动关闭

        Files.delete(p);
    }
}

异常链:包装而非吞掉

当底层异常对上层不友好(比如把 SQLException 暴露给 Controller 是不合适的),通过包装把底层异常作为 cause 保留下来。再加 throw new XxxException("业务说明", e),调用栈和原始异常都能在日志里看到。

// ExceptionChain.java
public class ExceptionChain {
    static String loadUser(long id) throws UserNotFoundException {
        try {
            // 假装是 SQL 查询
            if (id <= 0) throw new RuntimeException("SQL error: bad id");
            return "Alice";
        } catch (RuntimeException dbErr) {
            throw new UserNotFoundException("load user " + id + " failed", dbErr);
        }
    }

    public static void main(String[] args) {
        try {
            loadUser(-1);
        } catch (UserNotFoundException e) {
            System.out.println(e.getMessage());
            System.out.println("caused by: " + e.getCause());
            // e.printStackTrace(); // 完整链
        }
    }
}

class UserNotFoundException extends Exception {
    public UserNotFoundException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

自定义异常

业务层往往需要自己的异常类型,便于调用方按类型 catch。约定俗成:业务异常多继承 RuntimeException(unchecked,避免到处 throws 污染签名),命名以 Exception 结尾。

// CustomException.java
public class CustomException {
    public static void main(String[] args) {
        try {
            validate("");
        } catch (ValidationException e) {
            System.out.println(e.getField() + ": " + e.getMessage());
        }
    }

    static void validate(String name) {
        if (name == null || name.isBlank()) {
            throw new ValidationException("name", "must not be blank");
        }
    }
}

class ValidationException extends RuntimeException {
    private final String field;
    public ValidationException(String field, String message) {
        super(message);
        this.field = field;
    }
    public String getField() { return field; }
}

实践要点

  • 永远不要写空 catch 块;至少 log 一下
  • 不要 catch Exception 后什么都不做,再 throw 一个无 cause 的新异常——丢失栈信息
  • 业务流程出错抛 RuntimeException;库强制 caller 处理才用 checked
  • 底层异常对上层无意义时,包装:throw new XxxException("msg", originalException)
  • try-with-resources 一律替代手写 finally close
  • Spring 等框架几乎都把 checked 转成 unchecked——主流趋势是少用 checked