1. 概述

Java 异常主要分为两类:检查异常(Checked Exceptions)和非检查异常(Unchecked Exceptions)

本文将通过代码示例深入讲解它们的使用场景、区别以及设计时的权衡。对于有经验的开发者来说,这不仅是语法问题,更是系统健壮性和 API 设计哲学的体现。

2. 检查异常(Checked Exceptions)

检查异常代表的是程序本身无法控制的外部问题,比如文件不存在、网络连接失败、数据库查询出错等。这类异常在编译期就会被编译器强制检查,因此得名“检查异常”。

⚠️ 如果方法中可能抛出检查异常,你必须显式处理:要么用 try-catch 捕获,要么通过 throws 向上抛出。

例如,FileInputStream 构造函数在文件不存在时会抛出 FileNotFoundException

private static void checkedExceptionWithThrows() throws FileNotFoundException {
    File file = new File("not_existing_file.txt");
    FileInputStream stream = new FileInputStream(file);
}

上面这个方法没有自己处理异常,而是通过 throws 声明抛给调用方。调用者就必须面对这个异常,无法视而不见。

当然,也可以直接捕获处理:

private static void checkedExceptionWithTryCatch() {
    File file = new File("not_existing_file.txt");
    try {
        FileInputStream stream = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
}

常见的检查异常包括:

  • IOException
  • SQLException
  • ParseException

这些都属于外部环境导致的问题,调用方有可能恢复,比如重试、提示用户、切换备用资源等。

自定义检查异常

要创建自定义检查异常,只需继承 Exception 类即可:

public class IncorrectFileNameException extends Exception {
    public IncorrectFileNameException(String errorMessage) {
        super(errorMessage);
    }
}

这个异常适用于业务逻辑中可预见但需调用方干预的错误场景,比如文件名格式不合法。

3. 非检查异常(Unchecked Exceptions)

非检查异常通常反映程序内部的逻辑错误或编程疏忽,比如空指针、数组越界、除零操作等。它们继承自 RuntimeException,编译器不会强制你处理。

来看一个典型的除零例子:

private static void divideByZero() {
    int numerator = 1;
    int denominator = 0;
    int result = numerator / denominator;
}

这段代码编译完全没问题,但运行时会抛出 ArithmeticException。这就是非检查异常的“危险”之处——它躲过了编译器的审查,直到运行才暴露。

常见非检查异常包括:

  • NullPointerException
  • ArrayIndexOutOfBoundsException
  • IllegalArgumentException

它们通常意味着:代码写错了,或者传参不符合前置条件

自定义非检查异常

继承 RuntimeException 即可创建自定义非检查异常:

public class NullOrEmptyException extends RuntimeException {
    public NullOrEmptyException(String errorMessage) {
        super(errorMessage);
    }
}

这种异常适合用于参数校验失败等“程序 bug 级别”的错误,调用方无法合理恢复,只能修复代码。

4. 何时使用检查异常 vs 非检查异常

这个问题在 Java 社区长期存在争议,但 Oracle 官方文档给出了明确建议:

“如果调用方有可能合理地从异常中恢复,就使用检查异常;如果调用方无能为力,那就使用非检查异常。”

简单粗暴地说:

  • 能 recover → Checked
  • 不能 recover → Unchecked

使用检查异常的场景

假设你要打开一个文件,先校验文件名格式是否正确。如果格式不对,但用户可以重新输入,这就属于“可恢复”错误:

if (!isCorrectFileName(fileName)) {
    throw new IncorrectFileNameException("Incorrect filename : " + fileName );
}

调用方收到这个异常后,可以提示用户重新输入,系统继续运行。

使用非检查异常的场景

但如果传入的 fileNamenull 或空字符串,说明调用方根本没有做好前置校验,属于程序逻辑错误:

if (fileName == null || fileName.isEmpty())  {
    throw new NullOrEmptyException("The filename is null or empty.");
}

这种问题不应该靠“恢复”来解决,而应该在开发阶段就被发现和修复。让它抛出 RuntimeException,早点暴露问题,比静默失败或被吞掉强得多。

踩坑提醒

很多新手滥用检查异常,把所有错误都做成 checked,结果导致:

  • 方法签名越来越长,到处 throws
  • 调用链层层捕获又不处理,最后 printStackTrace() 一丢了之
  • 代码被异常处理逻辑淹没,主流程变得难以阅读

这其实是对异常机制的误用。记住:检查异常不是用来传递业务错误的,而是用来强制调用方面对外部风险的

5. 总结

特性 检查异常 非检查异常
是否编译期检查 ✅ 是 ❌ 否
继承自 Exception RuntimeException
典型场景 文件不存在、网络超时 空指针、参数非法
调用方可恢复? ✅ 可能 ❌ 几乎不能
是否必须处理 ✅ 必须 ❌ 不强制

合理选择异常类型,不仅能提升代码健壮性,还能让 API 更清晰地表达“这个错误你能怎么办”。别让异常成为代码的噪音。

所有示例代码已托管至 GitHub:https://github.com/baeldung/core-java/tree/master/core-java-exceptions