背景
本文是《Java 后端从小白到大神》修仙系列第十二篇,正式进入Java后端世界,本篇文章主要聊Java基础中的异常处理。若想详细学习请点击首篇博文,我们开始吧。
文章概览
- 异常处理概述
- 异常体系结构
- 异常处理机制
- 异常处理最佳实践
- 自定义异常
- 异常处理性能考虑
异常处理概述
异常是程序运行过程中发生的意外情况,可能导致程序中断或错误。Java 提供了一套完整的异常处理机制,用于捕获、处理和传递异常,确保程序能够优雅地处理错误情况。
什么是异常?
异常是程序执行过程中发生的事件,它中断了正常的指令流。在 Java 中,所有异常都是 Throwable 类的子类。
异常体系结构
Java 的异常体系结构是一个层次分明的类层次结构,所有异常都继承自 Throwable 类。
异常体系图
graph TD
Throwable --> Error
Throwable --> Exception
Exception --> RuntimeException
Exception --> IOException
Exception --> SQLException
RuntimeException --> NullPointerException
RuntimeException --> ArithmeticException
RuntimeException --> ArrayIndexOutOfBoundsException
RuntimeException --> IllegalArgumentException
Error --> OutOfMemoryError
Error --> StackOverflowError
异常分类
-
Error(错误):
- 严重的系统级错误,程序无法恢复
- 如
OutOfMemoryError、StackOverflowError
- 一般不需要捕获和处理
-
Exception(异常):
- 程序运行时的异常情况
- 分为检查型异常和非检查型异常
-
检查型异常(Checked Exception):
也叫:编译时异常
- 什么时候出现:编译期就强制检查
- 必须做什么:必须 try-catch 或 throws 声明,否则编译不通过
- 典型例子:IOException、SQLException
- 继承关系:extends Exception
- 但不是 RuntimeException 的子类
一句话记忆:编译器盯着你,不处理不让运行。
-
非检查型异常(Unchecked Exception):
也叫:运行时异常
- 什么时候出现:运行时才可能触发
- 必须做什么:不强制处理,代码可直接运行
- 典型例子:NullPointerException、ArrayIndexOutOfBoundsException
- 继承关系:extends RuntimeException
一句话记忆:
编译器不管你,代码写错才炸。
异常处理机制
1. try-catch 块
用于捕获并处理代码块中的异常。
1
2
3
4
5
6
7
8
9
|
try {
// 可能抛出异常的代码
int result = 10 / 0; // 抛出 ArithmeticException
} catch (ArithmeticException e) {
// 处理异常
System.out.println("错误:除数不能为零!");
// 打印异常信息
e.printStackTrace();
}
|
说明:
try 块:包含可能抛出异常的代码
catch 块:捕获并处理指定类型的异常
e:异常对象,包含异常信息
2. 多个 catch 块
处理不同类型的异常,子类异常需在前。
1
2
3
4
5
6
7
8
9
10
|
try {
int[] arr = new int[5];
arr[10] = 50; // 抛出 ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
// 处理数组越界异常
System.out.println("数组越界!");
} catch (Exception e) {
// 处理其他所有异常
System.out.println("其他异常:" + e.getMessage());
}
|
注意:
- 异常捕获顺序很重要,子类异常必须放在父类异常之前
- 否则子类异常将永远不会被捕获
3. finally 块
无论是否发生异常,都会执行的代码块(常用于资源清理)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
FileInputStream file = null;
try {
file = new FileInputStream("test.txt");
// 读取文件操作
} catch (FileNotFoundException e) {
System.out.println("文件未找到!");
} finally {
// 无论是否异常,都会执行
System.out.println("执行 finally 块");
// 关闭资源
if (file != null) {
try {
file.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
作用:
- 确保资源被正确释放
- 清理临时变量
- 执行必须完成的操作
4. throw 关键字
主动抛出异常(需在方法内处理或声明)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void checkAge(int age) {
if (age < 18) {
// 主动抛出异常
throw new IllegalArgumentException("年龄未满18岁!");
}
System.out.println("年龄验证通过");
}
// 调用示例
try {
checkAge(15);
} catch (IllegalArgumentException e) {
System.out.println("错误:" + e.getMessage());
}
|
注意:
throw 用于方法体内
- 抛出的是异常对象
- 可以抛出任何
Throwable 类型的异常
5. throws 关键字
声明方法可能抛出的异常(由调用者处理)。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 方法声明可能抛出 IOException
public void readFile() throws IOException {
FileReader file = new FileReader("test.txt");
// 读取文件操作
file.close();
}
// 调用示例
try {
readFile();
} catch (IOException e) {
System.out.println("IO异常:" + e.getMessage());
}
|
说明:
throws 用于方法签名
- 声明方法可能抛出的异常类型
- 可以声明多个异常,用逗号分隔
6. try-with-resources(Java 7+)
自动关闭资源(资源需实现 AutoCloseable 接口)。
1
2
3
4
5
6
7
8
9
10
11
|
// 自动关闭资源
try (FileInputStream fis = new FileInputStream("test.txt")) {
int data = fis.read();
while (data != -1) {
System.out.print((char) data);
data = fis.read();
}
} catch (IOException e) {
System.out.println("IO异常:" + e.getMessage());
}
// 无需显式关闭,资源自动关闭
|
优点:
支持的资源:
- 所有实现
AutoCloseable 接口的类
- 如
FileInputStream、FileOutputStream、Connection、Statement 等
异常处理最佳实践
1. 只捕获你能处理的异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 不推荐
try {
// 业务逻辑
} catch (Exception e) {
// 捕获所有异常但不处理
e.printStackTrace();
}
// 推荐
try {
// 业务逻辑
} catch (SpecificException e) {
// 针对性处理特定异常
handleSpecificException(e);
} catch (AnotherSpecificException e) {
// 处理另一种特定异常
handleAnotherException(e);
}
|
2. 合理使用异常类型
1
2
3
4
5
6
7
8
9
|
// 不推荐
if (age < 18) {
throw new Exception("年龄未满18岁");
}
// 推荐
if (age < 18) {
throw new IllegalArgumentException("年龄未满18岁");
}
|
3. 提供有意义的异常信息
1
2
3
4
5
|
// 不推荐
throw new IllegalArgumentException("参数错误");
// 推荐
throw new IllegalArgumentException("年龄必须大于等于18岁,当前值:" + age);
|
4. 避免在循环中抛出异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 不推荐
for (int i = 0; i < 1000; i++) {
try {
if (i == 500) {
throw new Exception("遇到错误");
}
} catch (Exception e) {
// 处理异常
}
}
// 推荐
for (int i = 0; i < 1000; i++) {
if (i == 500) {
// 处理错误
break;
}
}
|
5. 正确使用 finally 块
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
39
40
|
// 不推荐
String readFile() {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 读取文件
return "文件内容";
} catch (IOException e) {
// return 永远是方法最后一步
// 1. 异常进来了
// 2. 确定要返回:"错误"
return "错误";
} finally {
// finally 永远在 return 前面跑
// 3. 【先执行这里!,再执行 2】
System.out.println("执行 finally");
}
}
// 推荐
String readFile() {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 读取文件
return "文件内容";
} catch (IOException e) {
System.out.println("IO异常:" + e.getMessage());
return "错误";
} finally {
// 清理资源
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
|
自定义异常
创建继承自 Exception 或 RuntimeException 的类。
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
|
// 自定义检查型异常类
// 先调用父类构造,作用:给父类自己的那部分的成员变量赋初值,但不创建新对象!,只有new 构造函数的时候才创建对象。
// 再调用子类构造,作用:给子类自己的字段赋值、做额外初始化,但不创建新对象!,只有new 构造函数的时候才创建对象。
// 子类对象大小 = 父类字段 + 子类字段
class ValidationException extends Exception {
public ValidationException() {
super();
}
public ValidationException(String message) {
super(message);
}
public ValidationException(String message, Throwable cause) {
super(message, cause);
}
}
// 使用示例
void validateUser(String username, String password) throws ValidationException {
if (username == null || username.isEmpty()) {
throw new ValidationException("用户名不能为空");
}
if (password == null || password.length() < 6) {
throw new ValidationException("密码长度不能少于6位");
}
}
// 调用时捕获
try {
validateUser("", "123");
} catch (ValidationException e) {
System.out.println("验证失败:" + e.getMessage());
}
|
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
|
// 自定义非检查型异常类
class BusinessException extends RuntimeException {
private int errorCode;
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
// 使用示例
void processOrder(int orderId) {
if (orderId <= 0) {
throw new BusinessException("订单ID无效", 400);
}
// 处理订单
}
// 调用时可以选择捕获
processOrder(-1); // 直接抛出异常
|
3. 自定义异常的最佳实践
- 提供多个构造方法
- 包含错误码等额外信息
- 保持异常类的简洁性
- 合理命名异常类
异常处理性能考虑
1. 异常的性能影响
- 异常处理比正常流程慢
- 异常会产生堆栈跟踪,消耗内存
- 频繁抛出异常会影响程序性能
2. 性能优化建议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 不推荐 - 使用异常控制流程
public int divide(int a, int b) {
try {
return a / b;
} catch (ArithmeticException e) {
return 0;
}
}
// 推荐 - 先检查条件
public int divide(int a, int b) {
if (b == 0) {
return 0;
}
return a / b;
}
|
3. 异常处理的性能优化
- 只在真正异常的情况下使用异常
- 避免在循环中抛出异常
- 合理使用 try-catch 块的范围
- 优先使用检查型异常处理可恢复的错误
- 使用非检查型异常处理不可恢复的错误
异常处理常见问题
1. 空指针异常(NullPointerException)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 常见错误
String str = null;
System.out.println(str.length()); // 抛出 NullPointerException
// 避免方法
String str = null;
if (str != null) {
System.out.println(str.length());
}
// Java 8+ 可选
Optional<String> optionalStr = Optional.ofNullable(str);
optionalStr.ifPresent(s -> System.out.println(s.length()));
|
2. 数组越界异常(ArrayIndexOutOfBoundsException)
1
2
3
4
5
6
7
8
9
10
|
// 常见错误
int[] arr = new int[5];
System.out.println(arr[10]); // 抛出 ArrayIndexOutOfBoundsException
// 避免方法
int[] arr = new int[5];
int index = 10;
if (index >= 0 && index < arr.length) {
System.out.println(arr[index]);
}
|
3. 类型转换异常(ClassCastException)
1
2
3
4
5
6
7
8
9
10
11
|
// 常见错误
Object obj = "Hello";
Integer num = (Integer) obj; // 抛出 ClassCastException
// 避免方法
Object obj = "Hello";
if (obj instanceof Integer) {
Integer num = (Integer) obj;
} else {
System.out.println("类型不匹配");
}
|
总结
异常处理是 Java 编程中非常重要的一部分,它可以帮助我们:
- 优雅处理错误:捕获并处理异常,避免程序崩溃
- 提高代码可读性:使错误处理逻辑清晰可见
- 增强代码健壮性:处理各种异常情况,提高程序稳定性
- 便于调试:通过异常信息快速定位问题
掌握异常处理的核心概念和最佳实践,对于编写高质量的 Java 代码至关重要。在实际开发中,应该根据具体场景选择合适的异常处理方式,既要保证程序的正确性,又要考虑代码的性能和可维护性。