引言

异常处理是Java编程的基础,但很多开发者对异常处理存在误区。本文将深入探讨Java异常机制、最佳实践和常见陷阱,帮助你写出更健壮的代码。

一、Java异常体系

1.1 异常层次结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Throwable
├── Error(错误)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
└── Exception(异常)
├── RuntimeException(运行时异常/非受检异常)
│ ├── NullPointerException
│ ├── IndexOutOfBoundsException
│ ├── IllegalArgumentException
│ └── ArithmeticException
└── 受检异常(Checked Exception)
├── IOException
├── SQLException
└── ClassNotFoundException

1.2 Error vs Exception

Error(错误)

1
2
3
4
5
6
7
8
9
10
11
// Error是严重问题,程序无法处理
// 例如:OutOfMemoryError
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 最终OOM
}

// StackOverflowError
public void recursion() {
recursion(); // 无限递归
}

Exception(异常)

1
2
3
4
5
6
// Exception是可以被处理的问题
try {
int result = 10 / 0; // ArithmeticException
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
}

1.3 受检异常 vs 非受检异常

受检异常(Checked Exception)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 必须显式处理(try-catch或throws)
public void readFile() throws IOException {
FileReader reader = new FileReader("file.txt");
// ...
}

// 或者
public void readFile() {
try {
FileReader reader = new FileReader("file.txt");
} catch (IOException e) {
e.printStackTrace();
}
}

非受检异常(Unchecked Exception)

1
2
3
4
5
// 不强制处理
public void method() {
String str = null;
str.length(); // NullPointerException,不强制捕获
}

二、异常处理基础

2.1 try-catch-finally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void method() {
FileReader reader = null;
try {
reader = new FileReader("file.txt");
// 读取文件
} catch (FileNotFoundException e) {
System.err.println("文件不存在");
} catch (IOException e) {
System.err.println("读取失败");
} finally {
// 一定会执行(除非JVM崩溃)
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

多重catch的顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 错误:子类异常必须在前
try {
// ...
} catch (Exception e) { // 父类在前
// ...
} catch (IOException e) { // 子类在后,永远不会执行!编译错误
// ...
}

// 正确:子类在前,父类在后
try {
// ...
} catch (IOException e) { // 具体异常在前
// ...
} catch (Exception e) { // 通用异常在后
// ...
}

2.2 try-with-resources(JDK 7+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// JDK 7之前:需要手动关闭资源
FileReader reader = null;
try {
reader = new FileReader("file.txt");
// 使用reader
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

// JDK 7+:自动关闭资源
try (FileReader reader = new FileReader("file.txt")) {
// 使用reader
} catch (IOException e) {
e.printStackTrace();
}
// reader自动关闭

// 多个资源
try (FileInputStream fis = new FileInputStream("in.txt");
FileOutputStream fos = new FileOutputStream("out.txt")) {
// 使用资源
} catch (IOException e) {
e.printStackTrace();
}

自定义AutoCloseable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyResource implements AutoCloseable {

public void doSomething() {
System.out.println("doing something");
}

@Override
public void close() {
System.out.println("resource closed");
}
}

// 使用
try (MyResource resource = new MyResource()) {
resource.doSomething();
} // 自动调用close()

2.3 throws声明异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方法签名声明可能抛出的异常
public void readFile(String path) throws IOException, FileNotFoundException {
FileReader reader = new FileReader(path);
// ...
}

// 调用者必须处理
public void caller() {
try {
readFile("file.txt");
} catch (IOException e) {
e.printStackTrace();
}
}

// 或者继续throws
public void caller() throws IOException {
readFile("file.txt");
}

2.4 throw抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void setAge(int age) {
if (age < 0 || age > 150) {
// 主动抛出异常
throw new IllegalArgumentException("年龄必须在0-150之间");
}
this.age = age;
}

// 使用
try {
user.setAge(-1);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}

三、自定义异常

3.1 创建自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 业务异常
public class BusinessException extends RuntimeException {
private int code;

public BusinessException(int code, String message) {
super(message);
this.code = code;
}

public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}

public int getCode() {
return code;
}
}

// 使用
public void transfer(Long from, Long to, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException(4001, "转账金额必须大于0");
}

BigDecimal balance = getBalance(from);
if (balance.compareTo(amount) < 0) {
throw new BusinessException(4002, "余额不足");
}

// 执行转账
}

3.2 异常体系设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 基础异常类
public abstract class BaseException extends RuntimeException {
private int code;
private String message;

public BaseException(int code, String message) {
super(message);
this.code = code;
this.message = message;
}

// getters
}

// 具体业务异常
public class UserNotFoundException extends BaseException {
public UserNotFoundException(Long userId) {
super(40401, "用户不存在:" + userId);
}
}

public class InsufficientBalanceException extends BaseException {
public InsufficientBalanceException(BigDecimal balance, BigDecimal amount) {
super(40001, String.format("余额不足:当前余额%.2f,需要%.2f", balance, amount));
}
}

// 系统异常
public class SystemException extends BaseException {
public SystemException(String message) {
super(50000, message);
}

public SystemException(String message, Throwable cause) {
super(50000, message);
initCause(cause);
}
}

四、异常处理最佳实践

4.1 不要捕获Throwable或Error

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:捕获Throwable会捕获Error
try {
// ...
} catch (Throwable t) {
// 可能捕获OutOfMemoryError等严重错误
}

// ✅ 正确:只捕获Exception
try {
// ...
} catch (Exception e) {
// ...
}

4.2 不要吞掉异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 错误:空catch块
try {
// ...
} catch (Exception e) {
// 什么都不做,异常被吞掉
}

// ✅ 正确:至少记录日志
try {
// ...
} catch (Exception e) {
logger.error("操作失败", e);
}

// ✅ 或者重新抛出
try {
// ...
} catch (Exception e) {
logger.error("操作失败", e);
throw new BusinessException("业务处理失败", e);
}

4.3 不要使用异常进行流程控制

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:用异常控制逻辑
try {
while (true) {
array[index++];
}
} catch (ArrayIndexOutOfBoundsException e) {
// 到达数组末尾
}

// ✅ 正确:使用条件判断
for (int i = 0; i < array.length; i++) {
// 处理元素
}

4.4 及早失败(Fail-Fast)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ✅ 在方法开始处检查参数
public void process(String input) {
if (input == null || input.isEmpty()) {
throw new IllegalArgumentException("input不能为空");
}

// 继续处理
}

// ❌ 不好:延迟检查
public void process(String input) {
// 大量代码
// ...
// ...
if (input == null) { // 太晚了
throw new IllegalArgumentException("input不能为空");
}
}

4.5 提供有意义的异常信息

1
2
3
4
5
6
7
8
// ❌ 错误:信息不明确
throw new Exception("出错了");

// ✅ 正确:提供详细信息
throw new BusinessException(
40401,
String.format("用户不存在:userId=%d, username=%s", userId, username)
);

4.6 记录异常栈信息

1
2
3
4
5
6
7
8
9
// ❌ 错误:只记录消息
catch (Exception e) {
logger.error("错误:" + e.getMessage());
}

// ✅ 正确:记录完整栈信息
catch (Exception e) {
logger.error("操作失败", e); // 完整栈信息
}

4.7 不要在finally中抛异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 危险:finally中的异常会覆盖try中的异常
try {
throw new RuntimeException("try异常");
} finally {
throw new RuntimeException("finally异常"); // 覆盖try异常
}

// ✅ 正确:finally中捕获异常
try {
throw new RuntimeException("try异常");
} finally {
try {
// 清理资源
} catch (Exception e) {
logger.error("资源清理失败", e);
}
}

4.8 返回空集合而非null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 容易导致NullPointerException
public List<User> getUsers() {
return null;
}

List<User> users = getUsers();
users.size(); // NPE!

// ✅ 返回空集合
public List<User> getUsers() {
return Collections.emptyList();
}

List<User> users = getUsers();
users.size(); // 0,安全

五、Spring中的异常处理

5.1 全局异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
ErrorResponse error = new ErrorResponse(e.getCode(), e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
logger.error("系统异常", e);
ErrorResponse error = new ErrorResponse(50000, "系统错误");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
ErrorResponse error = new ErrorResponse(40000, message);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}

// 错误响应实体
@Data
@AllArgsConstructor
public class ErrorResponse {
private int code;
private String message;
}

5.2 统一返回格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Data
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;

public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}

public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
}

// Controller
@RestController
public class UserController {

@GetMapping("/users/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
if (user == null) {
throw new BusinessException(40401, "用户不存在");
}
return Result.success(user);
}
}

六、性能考虑

6.1 异常的性能开销

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 异常创建开销大:需要填充栈轨迹
public class PerformanceTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();

// 创建10万个异常
for (int i = 0; i < 100000; i++) {
try {
throw new Exception();
} catch (Exception e) {
// 捕获
}
}

long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms"); // 约1000ms
}
}

6.2 优化建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 避免在循环中抛异常
// ❌ 性能差
for (int i = 0; i < 10000; i++) {
try {
if (condition) {
throw new Exception();
}
} catch (Exception e) {
// ...
}
}

// ✅ 改进:提前判断
for (int i = 0; i < 10000; i++) {
if (condition) {
// 处理
} else {
// 处理
}
}

// 2. 复用异常对象(特殊场景)
private static final BusinessException INSUFFICIENT_BALANCE =
new BusinessException(40001, "余额不足");

public void transfer() {
if (balance < amount) {
throw INSUFFICIENT_BALANCE; // 复用,但丢失了栈信息
}
}

七、常见陷阱

7.1 finally中的return

1
2
3
4
5
6
7
8
// 陷阱:finally中的return会覆盖try中的return
public int method() {
try {
return 1;
} finally {
return 2; // 最终返回2,而不是1!
}
}

7.2 异常链断裂

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:异常链断裂
try {
// ...
} catch (SQLException e) {
throw new BusinessException("数据库错误"); // 原始异常丢失
}

// ✅ 正确:保留异常链
try {
// ...
} catch (SQLException e) {
throw new BusinessException("数据库错误", e); // 传递原始异常
}

7.3 过度使用受检异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ❌ 不好:强制调用者处理不太可能发生的异常
public User getUser(Long id) throws UserNotFoundException {
// ...
}

// 每次调用都要try-catch
try {
User user = userService.getUser(id);
} catch (UserNotFoundException e) {
// ...
}

// ✅ 改进:使用运行时异常
public User getUser(Long id) {
// 不强制处理
if (user == null) {
throw new BusinessException("用户不存在");
}
return user;
}

八、面试高频问题

8.1 Error和Exception的区别?

  • Error:严重问题,程序无法处理(如OOM)
  • Exception:可处理的异常,分为受检和非受检

8.2 finally一定会执行吗?

几乎一定执行,除非:

  1. System.exit()
  2. JVM崩溃
  3. 守护线程中断

8.3 try-catch-finally的执行顺序?

1
2
try → catch → finally → 返回值
注意:finally中修改返回值无效(基本类型和不可变对象)

总结

异常处理最佳实践:

  1. 合理选择异常类型:受检 vs 非受检
  2. 提供详细信息:帮助定位问题
  3. 不要吞掉异常:至少记录日志
  4. 及早失败:尽早检查参数
  5. 使用try-with-resources:自动关闭资源
  6. 避免在finally中抛异常:会覆盖原异常
  7. 性能考虑:避免在热点代码中频繁抛异常

参考资料