异常层次结构

Java 中所有异常都继承自 Throwable 类,分为两大分支:ErrorException

Throwable
├── Error (系统级错误,不可处理)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── NoClassDefFoundError
└── Exception (程序级异常,可处理)
    ├── IOException (受检异常)
    ├── SQLException (受检异常)
    └── RuntimeException (非受检异常)
        ├── NullPointerException
        ├── ArrayIndexOutOfBoundsException
        ├── ClassCastException
        └── IllegalArgumentException
  • Error:JVM 层面的错误,程序无法处理,也不应尝试捕获
  • Exception:程序可以捕获并处理的异常

受检异常 vs 非受检异常

类型 父类 编译器检查 典型场景
受检异常 (Checked) Exception (非 RuntimeException) 必须显式处理 IO操作、数据库操作
非受检异常 (Unchecked) RuntimeException 不强制处理 空指针、数组越界
// 受检异常:必须 try-catch 或 throws
public void readFile(String path) throws IOException {
    FileReader reader = new FileReader(path);
}

// 非受检异常:编译器不强制要求处理
public int divide(int a, int b) {
    return a / b; // 如果 b=0 抛出 ArithmeticException
}

try-catch-finally

最基础的异常处理结构:

public void demo() {
    try {
        int[] arr = new int[5];
        arr[10] = 1;                    // 抛出异常
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("数组越界:" + e.getMessage());
    } finally {
        System.out.println("无论如何都会执行");
    }
}

多个 catch 块的匹配规则:从上到下匹配,子类异常必须写在父类异常前面:

try {
    // 业务代码
} catch (FileNotFoundException e) {
    // 子类异常在前
} catch (IOException e) {
    // 父类异常在后
} catch (Exception e) {
    // 兜底
}

finally 的执行时机——即使在 try 或 catch 中执行了 return,finally 仍会执行:

public static int test() {
    try {
        return 1;
    } finally {
        System.out.println("finally 执行了");
    }
}
// 输出:finally 执行了
// 返回值:1

唯一不执行 finally 的情况:调用了 System.exit(0) 或 JVM 崩溃。

try-with-resources (JDK 7+)

自动关闭实现了 AutoCloseable 接口的资源,无需手动写 finally:

// 传统方式
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("test.txt"));
    System.out.println(br.readLine());
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (br != null) {
        try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// try-with-resources 方式(简洁且安全)
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
    System.out.println(br.readLine());
} catch (IOException e) {
    e.printStackTrace();
}

多个资源可以同时声明,用分号分隔:

try (FileInputStream fis = new FileInputStream("in.txt");
     FileOutputStream fos = new FileOutputStream("out.txt")) {
    // 读写操作
    // 两个流都会自动关闭(关闭顺序与声明顺序相反)
}

throw vs throws

关键字 位置 作用
throw 方法体内部 抛出一个异常对象
throws 方法签名上 声明方法可能抛出的异常类型
// throws:声明可能抛出的异常
public void checkAge(int age) throws IllegalArgumentException {
    if (age < 0) {
        // throw:实际抛出异常对象
        throw new IllegalArgumentException("年龄不能为负数:" + age);
    }
}

自定义异常

// 自定义受检异常
public class InsufficientBalanceException extends Exception {
    public InsufficientBalanceException(String message) {
        super(message);
    }
}

// 自定义非受检异常
public class BusinessException extends RuntimeException {
    private String errorCode;

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

    public String getErrorCode() {
        return errorCode;
    }
}

异常链

将一个异常作为另一个异常的原因,保留完整的调用栈:

try {
    // 数据库操作
} catch (SQLException e) {
    // 包装为业务异常并保留原始异常
    throw new BusinessException("DB001", "数据查询失败", e);
}

最佳实践

  1. 不要吞掉异常——永远不要写空的 catch 块:
// 反例
try {
    doSomething();
} catch (Exception e) {
    // 什么都不做,问题被隐藏
}

// 正例:至少记录日志
try {
    doSomething();
} catch (Exception e) {
    log.error("处理失败", e);
    throw new ServiceException("操作失败", e);
}
  1. 优先使用标准异常——能用 JDK 自带的就别自定义(如 IllegalArgumentExceptionIllegalStateException

  2. 异常信息要明确——包含关键参数和上下文:

throw new IllegalArgumentException("用户ID无效: " + userId);
  1. 尽早抛出,延迟捕获——底层发现问题立即抛出,在能处理的地方才捕获

  2. 不要在循环中使用 try-catch——把 try-catch 放在循环外面,避免性能损耗