Java 异常处理

真实程序一定会出错:文件读不到、网络断了、用户输入不是数字、内存不足。Java 用 try / catch / finally 优雅地捕获异常,让程序不会因一处错误而崩溃。本章带你彻底掌握 Java 异常处理。

1. try / catch / finally

这是最基础的异常处理结构:try 包裹可能出错的代码,catch 捕获特定异常,finally 无论是否异常都执行(用于清理资源)。

public class TryCatchDemo {
    public static void main(String[] args) {
        try {
            int n = Integer.parseInt("abc");    // 抛 NumberFormatException
            int result = 10 / n;
            System.out.println(result);
        } catch (NumberFormatException e) {
            // 捕获特定异常
            System.out.println("数字格式错误: " + e.getMessage());
        } catch (ArithmeticException e) {
            // 多重 catch:匹配第一个合适的
            System.out.println("算术错误: " + e.getMessage());
        } finally {
            // finally 块:无论是否异常都执行(常用于关资源)
            System.out.println("清理工作完成");
        }

        // Java 7+ 多异常合并捕获(用 | 分隔)
        try {
            int[] arr = null;
            System.out.println(arr.length);     // 抛 NullPointerException
        } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
            System.out.println("空指针或越界: " + e.getClass().getSimpleName());
        }
    }
}

几个要点:

2. throw 与 throws

两个关键字长得很像,含义完全不同:

public class ThrowDemo {

    // throws:声明方法可能抛出哪些受检异常(让调用方处理)
    public static int divide(int a, int b) throws ArithmeticException {
        if (b == 0) {
            // throw:主动抛出异常对象
            throw new ArithmeticException("除数不能为 0");
        }
        return a / b;
    }

    public static void main(String[] args) {
        try {
            divide(10, 0);
        } catch (ArithmeticException e) {
            System.out.println("捕获: " + e.getMessage());    // 捕获: 除数不能为 0
        }

        // 抛异常后,后面的代码不会执行(直接跳到 catch)
        // 这就是"非正常流程控制"
    }
}

3. Java 异常体系

// Java 异常体系(简化版):
//
//          Throwable
//          /       \
//        Error     Exception
//                   /     \
//             (其他)    RuntimeException
//
// Error:JVM 严重错误,程序不应捕获(OutOfMemoryError、StackOverflowError)
// Exception:程序应捕获处理的异常
//   - 受检异常(checked):非 RuntimeException,编译器强制 try/catch 或 throws
//       常见:IOException、SQLException、ClassNotFoundException
//   - 非受检异常(unchecked,RuntimeException 子类):编译器不强制
//       常见:NullPointerException、IllegalArgumentException、
//             ArrayIndexOutOfBoundsException、ArithmeticException

public class HierarchyDemo {
    // 受检异常必须 throws 声明
    public void readConfig() throws java.io.IOException {
        // ...
    }

    // 非受检异常无需声明
    public void checkNotNull(Object o) {
        if (o == null) {
            throw new NullPointerException("参数不能为 null");
        }
    }
}

受检 vs 非受检异常是 Java 独有的设计(也是争议点):

业界争议:很多团队(包括 Spring)把受检异常包装成非受检(RuntimeException),避免方法签名上写一堆 throws。

4. 自定义异常

实际业务中,你需要定义自己的异常类(如 BusinessExceptionUserNotFoundException),携带错误码、错误信息:

// 1. 自定义异常:继承 Exception(受检)或 RuntimeException(非受检)
class BusinessException extends Exception {
    private int code;       // 业务错误码

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

    public int getCode() { return code; }
}

class InvalidAgeException extends RuntimeException {
    public InvalidAgeException(String msg) {
        super(msg);
    }
}

public class CustomExceptionDemo {

    // 抛出受检异常必须 throws 声明
    public static void checkBalance(double balance) throws BusinessException {
        if (balance < 0) {
            throw new BusinessException(4001, "余额不能为负: " + balance);
        }
        System.out.println("余额正常: " + balance);
    }

    // RuntimeException 不必 throws
    public static void checkAge(int age) {
        if (age < 0 || age > 150) {
            throw new InvalidAgeException("年龄不合理: " + age);
        }
    }

    public static void main(String[] args) {
        try {
            checkBalance(-100);    // 受检异常必须捕获或继续 throws
        } catch (BusinessException e) {
            System.out.println("[" + e.getCode() + "] " + e.getMessage());
        }

        try {
            checkAge(200);         // 抛 RuntimeException
        } catch (InvalidAgeException e) {
            System.out.println(e.getMessage());
        }
    }
}

自定义异常的最佳实践:

5. try-with-resources(Java 7+)

资源(文件流、数据库连接、网络 socket)用完必须关,否则资源泄漏。Java 7 引入 try-with-resources,自动关闭实现了 AutoCloseable 的资源:

import java.io.*;

public class TryWithResources {

    // 老式写法(Java 7 之前):资源必须手动关闭,容易漏
    public static void oldStyle() throws IOException {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader("test.txt"));
            System.out.println(reader.readLine());
        } finally {
            if (reader != null) {
                try {
                    reader.close();           // 嵌套 try,丑陋
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    // Java 7+ try-with-resources:资源自动关闭(必须实现 AutoCloseable)
    public static void newStyle() throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
            System.out.println(reader.readLine());
        }   // 这里自动调 reader.close(),即使发生异常也会关
        // 可以同时声明多个资源,用分号隔开
    }

    public static void main(String[] args) {
        // 简单示例:读取标准输入
        try (var reader = new java.io.StringReader("hello")) {
            System.out.println((char) reader.read());   // h
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

强烈推荐所有 I/O 资源都用这种写法。它解决了三个老大难问题:

6. 异常处理的最佳实践

小结

这一章你掌握了 Java 异常处理的全部要点:try/catch/finally、throw/throws、异常体系、受检 vs 非受检、自定义异常、try-with-resources。下一篇我们看 Java IO——读文件、写文件、网络数据流。

← 上一篇 Java 集合框架

下一篇 Java 输入输出

✈️💬