为什么需要自定义异常?
使用自定义异常的主要好处包括:

- 代码更清晰:通过异常的名称,就能清楚地知道错误发生的原因。
UserNotFoundException比Exception更具描述性。 - 错误处理更精确:调用方可以根据不同的异常类型,编写不同的
catch块来处理特定的错误情况,实现更精细的错误恢复逻辑。 - 便于维护和调试:当程序出错时,一个有意义的异常名可以大大加快定位和修复问题的速度。
- API 设计更友好:如果你在编写一个库或框架,提供自定义异常可以让使用者更好地理解你的 API 可能会抛出哪些错误。
如何定义自定义异常?
自定义异常本质上就是一个普通的 Java 类,它需要继承 Java 标准库中的异常类。
基础语法
public class MyCustomException extends Exception {
// 构造方法
}
或者,如果你希望它是一个“非受检异常”(Unchecked Exception,即编译器不强制要求你处理的异常),可以继承 RuntimeException:
public class MyCustomRuntimeException extends RuntimeException {
// 构造方法
}
最佳实践
一个设计良好的自定义异常通常包含以下部分:
-
继承
Exception或RuntimeException:
(图片来源网络,侵删)- 继承
Exception:这会创建一个“受检异常”(Checked Exception),调用方必须使用try-catch块处理,或者在方法签名中使用throws关键字声明,适用于那些可预见的、调用方应该处理的错误,例如文件不存在、网络连接失败等。 - 继承
RuntimeException:这会创建一个“非受检异常”(Unchecked Exception),调用方不强制要求处理,适用于那些由程序内部逻辑错误导致的、通常无法恢复的错误,例如非法参数、空指针等。
- 继承
-
提供构造方法:至少应该提供一个无参构造方法,推荐提供带消息的构造方法和带消息及原因(cause)的构造方法,以便更好地传递错误信息。
完整示例
下面我们通过一个具体的例子来演示如何定义和使用自定义异常。
步骤 1:定义自定义异常
假设我们正在开发一个用户管理系统,当尝试查找一个不存在的用户时,我们希望抛出一个特定的异常。
UserNotFoundException.java (受检异常)

/**
* 自定义受检异常,当用户未找到时抛出。
* 继承自 Exception,调用方必须处理。
*/
public class UserNotFoundException extends Exception {
// 1. 无参构造方法
public UserNotFoundException() {
super();
}
// 2. 带错误消息的构造方法
public UserNotFoundException(String message) {
super(message);
}
// 3. 带错误消息和原因(cause)的构造方法
public UserNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
InvalidAgeException.java (非受检异常)
/**
* 自定义非受检异常,当用户年龄无效时抛出。
* 继承自 RuntimeException,调用方不强制处理。
*/
public class InvalidAgeException extends RuntimeException {
public InvalidAgeException() {
super();
}
public InvalidAgeException(String message) {
super(message);
}
public InvalidAgeException(String message, Throwable cause) {
super(message, cause);
}
}
步骤 2:在业务逻辑中抛出自定义异常
我们创建一个服务类 UserService,它在特定条件下抛出我们刚刚定义的异常。
UserService.java
import java.util.HashMap;
import java.util.Map;
public class UserService {
// 模拟一个用户数据库
private Map<String, Integer> userDatabase = new HashMap<>();
public UserService() {
userDatabase.put("alice", 30);
userDatabase.put("bob", 25);
}
/**
* 根据用户名查找用户年龄
* @param username 用户名
* @return 用户年龄
* @throws UserNotFoundException 如果用户不存在,则抛出此异常
*/
public int findUserAge(String username) throws UserNotFoundException {
// 检查年龄是否为负数(这是一个程序逻辑错误,适合用非受检异常)
if (username == null || username.trim().isEmpty()) {
throw new InvalidAgeException("用户名不能为空或null");
}
Integer age = userDatabase.get(username);
if (age == null) {
// 抛出自定义受检异常
throw new UserNotFoundException("用户 '" + username + "' 未找到。");
}
// 模拟一个业务规则:年龄不能超过 120
if (age > 120) {
throw new InvalidAgeException("用户 '" + username + "' 的年龄 " + age + " 无效,不能超过120。");
}
return age;
}
}
步骤 3:调用方处理自定义异常
我们创建一个 Main 类来调用 UserService,并处理可能发生的异常。
Main.java
public class Main {
public static void main(String[] args) {
UserService userService = new UserService();
// --- 场景1:处理受检异常 ---
System.out.println("--- 场景1:查找存在的用户 ---");
try {
int age = userService.findUserAge("alice");
System.out.println("alice 的年龄是: " + age);
} catch (UserNotFoundException e) {
// 捕获并处理我们自定义的 UserNotFoundException
System.err.println("错误捕获: " + e.getMessage());
// 这里可以添加用户友好的提示,或者记录日志等
}
System.out.println("\n--- 场景2:处理受检异常(用户不存在)---");
try {
int age = userService.findUserAge("charlie"); // charlie 不存在
System.out.println("charlie 的年龄是: " + age);
} catch (UserNotFoundException e) {
System.err.println("错误捕获: " + e.getMessage());
}
// --- 场景3:处理非受检异常 ---
System.out.println("\n--- 场景3:处理非受检异常(年龄无效)---");
try {
// alice 的年龄是 30,这里我们模拟一个无效年龄
// 为了演示,我们直接调用会抛出 InvalidAgeException 的逻辑
// 这个异常可能是在 findUserAge 方法内部某处抛出的
// 如果我们有一个设置年龄的方法
System.out.println("尝试获取一个年龄过大的用户...");
// 为了简单起见,我们直接模拟抛出
throw new InvalidAgeException("年龄 150 无效");
} catch (InvalidAgeException e) {
// 捕获并处理我们自定义的 InvalidAgeException
System.err.println("错误捕获: " + e.getMessage());
}
System.out.println("\n--- 场景4:处理非受检异常(空用户名)---");
try {
int age = userService.findUserAge(""); // 传入空用户名
System.out.println("空用户的年龄是: " + age);
} catch (InvalidAgeException e) {
System.err.println("错误捕获: " + e.getMessage());
}
}
}
运行结果
--- 场景1:查找存在的用户 ---
alice 的年龄是: 30
--- 场景2:处理受检异常(用户不存在)---
错误捕获: 用户 'charlie' 未找到。
--- 场景3:处理非受检异常(年龄无效)---
尝试获取一个年龄过大的用户...
错误捕获: 年龄 150 无效
--- 场景4:处理非受检异常(空用户名)---
错误捕获: 用户名不能为空或null
高级特性:自定义异常与 cause
有时,一个异常是由另一个异常引起的,你的方法在尝试从文件读取用户数据时,文件不存在,导致抛出 FileNotFoundException,你希望将这个底层异常作为你自定义异常的原因(cause)包装起来,以便调用方可以追溯问题的根本原因。
这可以通过带 Throwable cause 参数的构造方法实现。
示例:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileUserService {
public void loadUsersFromFile(String filePath) throws UserNotFoundException {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
// ... 读取文件逻辑 ...
System.out.println("用户文件加载成功。");
} catch (IOException e) {
// 将底层的 IOException 作为 cause,包装成我们的自定义异常
throw new UserNotFoundException("无法从文件 '" + filePath + "' 加载用户数据。", e);
}
}
}
在调用方,你可以这样获取原始异常:
public class Main {
public static void main(String[] args) {
FileUserService service = new FileUserService();
try {
service.loadUsersFromFile("users.txt"); // 假设这个文件不存在
} catch (UserNotFoundException e) {
System.err.println("捕获到自定义异常: " + e.getMessage());
// 检查是否存在原因
if (e.getCause() != null) {
System.err.println("根本原因: " + e.getCause().getClass().getName() + " - " + e.getCause().getMessage());
}
}
}
}
输出:
捕获到自定义异常: 无法从文件 'users.txt' 加载用户数据。
根本原因: java.io.FileNotFoundException - users.txt (系统找不到指定的文件。)
- 继承:自定义异常通过继承
Exception(受检)或RuntimeException(非受检)来创建。 - 构造方法:提供至少一个无参构造方法和一个带消息的构造方法,最好再提供一个带
cause的构造方法。 - 抛出:在
throw new MyCustomException("message")的地方使用。 - 处理:在
try-catch块中捕获,或使用throws声明受检异常。 - 选择类型:根据错误是可恢复的业务逻辑问题(用受检异常)还是程序内部错误(用非受检异常)来决定继承哪个父类。
- 传递原因:使用
cause参数来包装底层异常,有助于问题追踪。
