1. 引言
Java 标准库中已经有很多异常:NullPointerException、IllegalArgumentException、IOException 等等。但有时标准异常不足以清晰地描述你程序中出现的错误。
实际示例:
你在编写一个银行应用。用户尝试提取的金额大于其账户余额。你可以抛出 IllegalArgumentException。但如果提供自定义异常,例如 InsufficientFundsException,会更清晰——读代码就能立刻看出发生了什么。
自定义异常是对应用的一种正确而有力的定制。它们可以精细地处理问题,而且从它们的名字(如果命名合理!)就能立刻明白发生了什么。此外,它们具有自文档化特性:方法签名中带有 throws MyException 会直接表明可能出现的错误。并且——你总可以添加额外字段(例如余额、操作金额等)。
2. 如何创建自定义异常?
很简单:创建一个新类,从某个标准异常类继承即可。
- 对于受检(checked)异常——继承自 Exception。
- 对于非受检(unchecked)异常——继承自 RuntimeException。
示例:受检异常
public class InvalidCredentialsException extends Exception {
public InvalidCredentialsException(String message) {
super(message); // 将消息传递给父类
}
}
现在你可以在代码中抛出该异常:
if (!login.equals("admin") || !password.equals("1234")) {
throw new InvalidCredentialsException("用户名或密码不正确");
}
示例:非受检异常
public class NegativeBalanceException extends RuntimeException {
public NegativeBalanceException(String message) {
super(message);
}
}
何时使用受检异常,何时使用非受检异常?
- 受检异常——当错误是可预期且可以被处理时(例如,校验错误、文件缺失、用户数据不正确)。
- 非受检异常——当错误与程序逻辑中的缺陷相关(例如,除以零、破坏不变式)。
3. 构造函数:如何让异常更有信息量
通常在自定义异常类中,至少会实现一个带有 String message 参数的构造函数。但往往还会添加其他构造函数:
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException() {
super();
}
public ScoreLimitExceededException(String message) {
super(message);
}
public ScoreLimitExceededException(String message, Throwable cause) {
super(message, cause);
}
public ScoreLimitExceededException(Throwable cause) {
super(cause);
}
}
说明:
- message —— 错误的文字描述。
- cause —— 原因(另一个异常),如果你想把一个错误“包装”进另一个异常中。
建议:如果不确定需要哪些构造函数,至少添加一个接收字符串的构造函数。
4. 在代码中使用自定义异常
来看一个示例:我们有一个用户,用户有积分,且不能添加到超过 100。
public class User {
private String name;
private int score;
public User(String name) {
this.name = name;
this.score = 0;
}
public void addScore(int points) throws ScoreLimitExceededException {
if (score + points > 100) {
throw new ScoreLimitExceededException("超出分数上限!尝试添加: " + points);
}
this.score += points;
}
}
异常类:
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException(String message) {
super(message);
}
}
处理:
try {
user.addScore(60);
user.addScore(50); // 这里将抛出异常!
} catch (ScoreLimitExceededException e) {
System.out.println("错误: " + e.getMessage());
}
结果:
错误: 超出分数上限!尝试添加: 50
你也许会问:如果不使用异常,改用 if 条件并返回 false 或其他特殊值来表示操作失败行不行?例如:
public boolean addScore(int points) {
if (score + points > 100) {
return false; // 或者抛出某个 RuntimeException,如果不想处理的话
}
this.score += points;
return true;
}
这种方式看起来更简单,但当涉及严重错误或破坏程序逻辑时,它有明显的不足。
首先,返回 false 或其他值来表示错误,会要求调用方始终检查返回值。如果程序员忘了检查,错误可能被悄然忽略,导致程序行为不可预测。相反,异常会强制处理(对于受检异常),或者至少在未捕获时明确地发出信号。
其次,异常能更清晰地传达错误语义。返回 false 可能意味着很多情况:“未成功”“不适用”“不可用”。而 ScoreLimitExceededException 则明确表示“分数上限已被突破”。这能提升代码的可读性与可维护性。
第三,异常允许集中化处理错误。与其在各个调用 addScore 的地方分散写 if 判断,不如在某个集中位置捕获异常并作出统一处理:向用户显示消息、记录日志或回滚事务。
最后,对于超限这类问题,它的确是异常情况(异常之名由此而来)。程序的正常执行路径假定积分能够成功添加。如果不行——这就是对业务逻辑或对象不变式的违反,正是使用异常的理想场景。
5. 在异常中添加自定义字段
有时在自定义异常中添加额外数据会很有帮助,这些数据能辅助错误处理。
示例:
public class ScoreLimitExceededException extends Exception {
private int currentScore;
private int attemptedAdd;
public ScoreLimitExceededException(String message, int currentScore, int attemptedAdd) {
super(message);
this.currentScore = currentScore;
this.attemptedAdd = attemptedAdd;
}
public int getCurrentScore() {
return currentScore;
}
public int getAttemptedAdd() {
return attemptedAdd;
}
}
用法:
if (score + points > 100) {
throw new ScoreLimitExceededException(
"超出分数上限!",
this.score,
points
);
}
6. 实用细节
应该如何给自定义异常命名?
在 Java 中,通常给自定义异常加上后缀 Exception: InvalidUserInputException、InsufficientFundsException、ScoreLimitExceededException。
不要把异常命名为 Error 或 Warning —— 这会误导其他开发者(也可能在几周后误导你自己)。
在哪里、何时抛出自定义异常?
- 在校验用户数据时(例如,姓名为空、年龄为负)。
- 在违反业务规则时(例如,超出限额、尝试提取超过账户余额的金额)。
- 在与外部服务交互出错时(例如,服务不可用、超时)。
7. 创建自定义异常时的常见错误
错误 1:继承自错误的类。
应从 Exception(或 RuntimeException)继承,而不是从 Throwable 或 Error 继承。
错误 2:未添加带消息的构造函数。
没有接受字符串(String message)的构造函数,你的异常将没有说明信息,调试会变得困难。
错误 3:用标准异常处理业务逻辑。
在需要自定义“更具表达性”的异常的地方,不要抛出 NullPointerException 或 IllegalArgumentException。
错误 4:滥用自定义异常。
不要为每个小问题都创建一个单独的异常类。如果错误并非你的领域特有,请使用标准异常。
错误 5:缺少序列化(不常见,但会发生)。
如果你的异常需要通过网络传输或被持久化,建议实现 implements Serializable。不过对于简单应用并非必需。
GO TO FULL VERSION