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 );
}
调用方收到这个异常后,可以提示用户重新输入,系统继续运行。
使用非检查异常的场景
但如果传入的 fileName
是 null
或空字符串,说明调用方根本没有做好前置校验,属于程序逻辑错误:
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