为什么需要自定义异常?
Java 提供了丰富的内置异常类,如 NullPointerException, ArrayIndexOutOfBoundsException, IOException 等,这些异常通常用于处理一些通用的、常见的错误情况。

但在实际开发中,你可能会遇到一些业务逻辑上的特定错误,这些错误无法用现有的异常类来准确描述。
- 用户输入的身份证号格式不正确。
- 账户余额不足以支付订单。
- 订单状态无法从“待发货”变为“已取消”。
在这种情况下,使用自定义异常可以带来以下好处:
- 代码更清晰:通过异常的名称就能直观地知道错误的原因,
InsufficientBalanceException比RuntimeException更具描述性。 - 错误处理更精确:调用方可以根据不同的异常类型,采取不同的处理逻辑。
InvalidInputException可能需要提示用户重新输入,而InsufficientBalanceException可能需要提示用户充值。 - 业务逻辑与异常处理解耦:将业务逻辑的错误(如余额不足)与系统级错误(如网络中断)分离开,使代码结构更清晰。
- 便于调试和日志记录:在日志中记录具体的自定义异常,能更快地定位问题。
如何创建自定义异常?
创建自定义异常非常简单,只需继承 Java 中已有的异常类即可。
继承哪个类?
这是最关键的一步,通常有以下几种选择:

-
继承
Exception:- 这是一个受检异常(Checked Exception)。
- 它强制调用方使用
try-catch块进行处理,或者在方法签名中使用throws关键字声明。 - 适用场景:当错误情况是可以预见的,并且调用方有能力或有必要恢复时,文件不存在、网络连接超时、数据库连接失败等。
-
继承
RuntimeException:- 这是一个非受检异常(Unchecked Exception)。
- 调用方不是必须处理它,如果不处理,程序可能会在运行时抛出异常而终止。
- 适用场景:通常表示编程错误或不可恢复的系统错误,空指针访问、非法的数组索引,业务逻辑上的错误,如果认为调用方不应该忽略,也可以使用
RuntimeException。
总结建议:
- 对于业务逻辑错误,通常推荐继承
RuntimeException,因为它更灵活,减少了代码的繁琐性。 - 对于系统级或外部资源相关的错误,推荐继承
Exception,强制调用方处理。
创建步骤
- 创建一个新类。
- 继承
Exception或RuntimeException。 - (可选)提供构造方法:
- 至少提供一个无参构造方法。
- 通常提供一个带字符串参数的构造方法,用于设置异常的描述信息。
- (推荐)提供一个带字符串参数和
Throwable原因的构造方法,以便于异常链的传递。
代码示例
下面我们通过一个完整的例子来演示如何创建和使用自定义异常。

场景:模拟一个简单的银行账户取款操作。
创建自定义异常类
我们将创建一个 InsufficientBalanceException(余额不足异常),它继承自 RuntimeException。
// InsufficientBalanceException.java
/**
* 自定义异常:余额不足
* 继承 RuntimeException,这是一个非受检异常。
*/
public class InsufficientBalanceException extends RuntimeException {
// 1. 无参构造方法
public InsufficientBalanceException() {
super();
}
// 2. 带有错误信息的构造方法
public InsufficientBalanceException(String message) {
super(message);
}
// 3. 带有错误信息和原因(另一个异常)的构造方法,用于异常链
public InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}
创建业务逻辑类
这个类包含一个取款方法,当余额不足时,会抛出我们刚刚定义的自定义异常。
// BankAccount.java
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
if (initialBalance < 0) {
// 这里可以抛出一个非法参数异常
throw new IllegalArgumentException("初始余额不能为负数");
}
this.balance = initialBalance;
}
/**
* 取款方法
* @param amount 要取出的金额
* @throws InsufficientBalanceException 当余额不足时抛出
*/
public void withdraw(double amount) {
// 检查取款金额是否合法
if (amount <= 0) {
throw new IllegalArgumentException("取款金额必须大于0");
}
// 检查余额是否充足
if (balance < amount) {
// 抛出自定义异常
throw new InsufficientBalanceException("取款失败:账户余额不足,当前余额: " + balance + ", 尝试取款: " + amount);
}
// 如果一切正常,执行取款操作
balance -= amount;
System.out.println("取款成功!当前余额: " + balance);
}
public double getBalance() {
return balance;
}
}
创建测试类来调用和捕获异常
这个类演示了如何调用 BankAccount 的方法,并处理可能发生的自定义异常。
// Main.java
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount(1000.0);
// --- 场景1:正常取款 ---
System.out.println("--- 场景1:正常取款 ---");
try {
account.withdraw(500.0);
} catch (InsufficientBalanceException e) {
// 因为余额是充足的,所以这段代码通常不会执行
System.err.println("捕获到异常: " + e.getMessage());
}
System.out.println("操作后的余额: " + account.getBalance());
System.out.println("----------------------------------\n");
// --- 场景2:余额不足,触发自定义异常 ---
System.out.println("--- 场景2:余额不足 ---");
try {
// 尝试取出比余额更多的钱
account.withdraw(800.0);
} catch (InsufficientBalanceException e) {
// 捕获我们自定义的异常
System.err.println("错误!捕获到自定义异常: " + e.getMessage());
// 在这里可以执行恢复逻辑,比如通知用户、记录日志等
} catch (IllegalArgumentException e) {
// 捕获非法参数异常
System.err.println("参数错误: " + e.getMessage());
} finally {
// finally 块中的代码无论是否发生异常都会执行
System.out.println("场景2处理完毕,当前余额: " + account.getBalance());
}
System.out.println("----------------------------------\n");
// --- 场景3:传入非法参数 ---
System.out.println("--- 场景3:传入非法参数 ---");
try {
account.withdraw(-100.0);
} catch (InsufficientBalanceException e) {
System.err.println("捕获到余额不足异常: " + e.getMessage());
} catch (IllegalArgumentException e) {
// 捕获非法参数异常
System.err.println("错误!捕获到参数异常: " + e.getMessage());
} finally {
System.out.println("场景3处理完毕,当前余额: " + account.getBalance());
}
}
}
运行结果
--- 场景1:正常取款 ---
取款成功!当前余额: 500.0
操作后的余额: 500.0
----------------------------------
--- 场景2:余额不足 ---
错误!捕获到自定义异常: 取款失败:账户余额不足,当前余额: 500.0, 尝试取款: 800.0
场景2处理完毕,当前余额: 500.0
----------------------------------
--- 场景3:传入非法参数 ---
错误!捕获到参数异常: 取款金额必须大于0
场景3处理完毕,当前余额: 500.0
最佳实践
- 保持异常的粒度:为每种特定的错误情况创建一个独特的异常类。
InvalidEmailException和InvalidPasswordException比InvalidUserInputException更好。 - 提供有意义的错误信息:在构造异常时,传入清晰的描述性字符串,方便日志记录和问题排查。
- 优先使用非受检异常:对于大多数应用层代码,继承
RuntimeException可以避免过多的try-catch或throws声明,使代码更简洁,只有当错误是可恢复的并且调用方必须处理时,才使用受检异常。 - 使用异常链:当一个异常是由另一个异常引起时(在解析数据库数据时发生
SQLException),使用cause参数将原始异常传递给新异常,有助于追踪问题的根本原因。 - 不要用异常来控制流程:异常机制是为处理错误而设计的,而不是用于常规的程序流程控制(如循环),用异常来处理
if-else能搞定的逻辑会严重影响性能。
