Java 异常处理最佳实践
异常处理是 Java 编程的重要部分,良好的异常处理能提高代码健壮性和可维护性。
一、异常体系
1.1 异常层次结构
graph TB
A[Throwable] --> B[Error]
A --> C[Exception]
B --> B1[OutOfMemoryError]
B --> B2[StackOverflowError]
B --> B3[VirtualMachineError]
C --> C1[RuntimeException]
C --> C2[Checked Exception]
C1 --> C11[NullPointerException]
C1 --> C12[IllegalArgumentException]
C1 --> C13[IndexOutOfBoundsException]
C2 --> C21[IOException]
C2 --> C22[SQLException]
C2 --> C23[ClassNotFoundException]
1.2 Error vs Exception
| 特性 | Error | Exception |
|---|---|---|
| 含义 | JVM 错误,无法恢复 | 程序错误,可处理 |
| 例子 | OOM、StackOverflow | IOException、SQLException |
| 处理 | 不应捕获 | 应捕获处理 |
| 恢复 | 无法恢复 | 可恢复 |
1.3 Checked vs Unchecked
// Checked Exception(必须处理)
try {
FileInputStream fis = new FileInputStream("file.txt");
} catch (FileNotFoundException e) {
// 必须捕获或声明抛出
}
// Unchecked Exception(运行时异常)
List<String> list = null;
list.size(); // NullPointerException,可不处理
常见 Checked 异常:
- IOException
- SQLException
- ClassNotFoundException
- InterruptedException
常见 Unchecked 异常:
- NullPointerException
- IllegalArgumentException
- IndexOutOfBoundsException
- ClassCastException
二、异常处理机制
2.1 try-catch-finally
try {
// 可能抛出异常的代码
FileInputStream fis = new FileInputStream("file.txt");
fis.read();
} catch (FileNotFoundException e) {
// 处理特定异常
e.printStackTrace();
} catch (IOException e) {
// 处理其他 IO 异常
e.printStackTrace();
} finally {
// 总是执行(清理资源)
System.out.println("Cleanup");
}
2.2 try-with-resources(JDK 7+)
// 自动关闭资源
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
String line = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
// 自动关闭,无需 finally
// 要求:资源实现 AutoCloseable 接口
2.3 多重捕获(JDK 7+)
// 之前
try {
// ...
} catch (IOException e) {
handle(e);
} catch (SQLException e) {
handle(e);
}
// JDK 7+
try {
// ...
} catch (IOException | SQLException e) {
handle(e); // e 是 final 的
}
2.4 异常重新抛出(JDK 7+)
public void process() throws IOException, SQLException {
try {
// ...
} catch (Exception e) {
// 编译器能推断具体类型
throw e;
}
}
三、throw vs throws
3.1 throw(抛出异常)
// 手动抛出异常
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
this.age = age;
}
3.2 throws(声明异常)
// 声明可能抛出的异常
public void readFile(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
fis.read();
fis.close();
}
// 调用者必须处理
try {
readFile("file.txt");
} catch (IOException e) {
e.printStackTrace();
}
四、自定义异常
4.1 创建自定义异常
// Checked 异常
public class BusinessException extends Exception {
private String code;
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
// Unchecked 异常
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}
4.2 使用自定义异常
public class UserService {
public void register(String username) throws BusinessException {
if (username == null || username.isEmpty()) {
throw new BusinessException("USER_001", "用户名不能为空");
}
// ...
}
}
// 调用
try {
userService.register("");
} catch (BusinessException e) {
System.out.println("Error " + e.getCode() + ": " + e.getMessage());
}
五、异常链
5.1 保留原始异常
// ❌ 丢失原始异常
public void process() {
try {
// ...
} catch (SQLException e) {
throw new RuntimeException("Database error"); // 丢失原始异常
}
}
// ✅ 保留原始异常
public void process() {
try {
// ...
} catch (SQLException e) {
throw new RuntimeException("Database error", e); // 保留因果
}
}
5.2 获取原始异常
try {
process();
} catch (RuntimeException e) {
Throwable cause = e.getCause(); // 获取原始异常
if (cause instanceof SQLException) {
// 处理 SQL 异常
}
}
六、最佳实践
6.1 捕获具体的异常
// ❌ 捕获过于宽泛
try {
// ...
} catch (Exception e) {
e.printStackTrace();
}
// ✅ 捕获具体异常
try {
// ...
} catch (FileNotFoundException e) {
log.error("File not found", e);
} catch (IOException e) {
log.error("IO error", e);
}
6.2 不要忽略异常
// ❌ 忽略异常
try {
// ...
} catch (IOException e) {
// 什么都不做
}
// ✅ 处理异常
try {
// ...
} catch (IOException e) {
log.error("IO error", e);
throw new RuntimeException("Failed to process", e);
}
6.3 不要吞掉异常
// ❌ 吞掉异常
public void process() {
try {
// ...
} catch (Exception e) {
e.printStackTrace(); // 只是打印,不处理
}
}
// ✅ 向上抛出
public void process() throws Exception {
try {
// ...
} catch (Exception e) {
log.error("Error", e);
throw e; // 或包装后抛出
}
}
6.4 使用标准异常
// ❌ 自定义不必要的异常
public class NullValueException extends Exception {}
// ✅ 使用标准异常
if (value == null) {
throw new IllegalArgumentException("Value cannot be null");
}
6.5 优先使用 Unchecked 异常
// ✅ 业务异常使用 RuntimeException
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
// 调用者可选择性处理
try {
service.process();
} catch (BusinessException e) {
// 可选处理
}
6.6 finally 中不要抛出异常
// ❌ finally 抛出异常
try {
// ...
} finally {
close(); // 可能抛出异常,掩盖 try 中的异常
}
// ✅ 正确处理
try {
// ...
} finally {
try {
close();
} catch (IOException e) {
log.error("Close failed", e);
}
}
6.7 异常日志记录
// ❌ 只记录消息
catch (Exception e) {
log.error("Error: " + e.getMessage());
}
// ✅ 记录完整堆栈
catch (Exception e) {
log.error("Error processing request", e);
}
七、常见误区
7.1 过度使用异常
// ❌ 异常用于控制流程
try {
return list.get(index);
} catch (IndexOutOfBoundsException e) {
return defaultValue;
}
// ✅ 先检查
if (index >= 0 && index < list.size()) {
return list.get(index);
}
return defaultValue;
7.2 异常性能开销
// 异常创建开销大
// 避免在循环中频繁创建异常
// ❌ 性能差
for (int i = 0; i < 10000; i++) {
try {
validate(i);
} catch (ValidationException e) {
// ...
}
}
// ✅ 先验证
for (int i = 0; i < 10000; i++) {
if (!isValid(i)) {
continue;
}
process(i);
}
八、总结
异常处理核心要点:
| 原则 | 说明 |
|---|---|
| 捕获具体异常 | 不要捕获 Exception |
| 不要忽略异常 | 必须处理或记录 |
| 保留异常链 | 包装时传入原始异常 |
| 优先 Unchecked | 业务异常用 RuntimeException |
| 使用 try-with-resources | 自动关闭资源 |
| 记录完整堆栈 | log.error(“msg”, e) |
良好的异常处理能提高代码健壮性,遵循最佳实践让代码更优雅。