异常处理
# 10.异常处理
# 目录介绍
# 10.1 异常入门介绍
# 10.1.1 异常概念解释
程序的错误大致可以分为三种:
- 语法错误:编译阶段就能发现,如拼写错误、缺少分号等。
- 逻辑错误:程序能运行但结果不对,通过调试解决。
- 运行时错误:程序运行期间发生的错误,如除数为0、空指针、数组越界等。
Java 通过异常机制处理运行时错误,让程序有机会优雅地处理错误,而不是直接崩溃。
异常处理的底层原理:Java 的异常处理在字节码中通过异常表(Exception Table) 实现,而不是像某些语言使用 setjmp/longjmp。每个方法的 Code 属性中都包含一个异常表,记录了 try 块的起始/结束 PC、catch 处理器的入口 PC 和捕获的异常类型。当异常抛出时,JVM 在当前方法的异常表中查找匹配项;若未找到,则弹出当前栈帧,沿调用栈向上查找(栈展开),直到找到匹配的 handler 或到达栈顶(线程终止)。throw new Exception() 的开销主要来自填充异常栈轨迹(fillInStackTrace()),需要遍历整个调用栈——这就是为什么不应该用异常控制正常业务流程。
对比 C++:C++ 也有 try-catch 异常机制,但 C++ 没有"检查异常"的概念,异常处理不是强制的。Java 的异常处理更加规范和严格。
# 10.1.2 异常体系结构
Throwable
├── Error(严重错误,程序无法处理)
│ ├── OutOfMemoryError(内存溢出)
│ ├── StackOverflowError(栈溢出)
│ └── ...
└── Exception(异常,程序可以处理)
├── RuntimeException(运行时异常/非检查异常)
│ ├── NullPointerException(空指针)
│ ├── ArrayIndexOutOfBoundsException(数组越界)
│ ├── ArithmeticException(算术异常)
│ ├── ClassCastException(类型转换异常)
│ └── ...
└── 检查异常(编译时必须处理)
├── IOException(IO异常)
├── SQLException(数据库异常)
├── FileNotFoundException(文件未找到)
└── ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
关键区别:
- 检查异常:编译器强制要求处理(try-catch 或 throws),如
IOException。 - 运行时异常:编译器不强制要求处理,如
NullPointerException。
# 10.1.3 异常基本语法
try {
// 可能会抛出异常的代码
} catch (异常类型 变量名) {
// 异常处理代码
} finally {
// 无论是否异常都会执行(可选)
}
2
3
4
5
6
7
# 10.1.4 捕获异常
public class Main {
public static void main(String[] args) {
try {
int result = 10 / 0; // ArithmeticException
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("发生算术异常:" + e.getMessage());
}
System.out.println("程序继续执行");
}
}
// 输出:
// 发生算术异常:/ by zero
// 程序继续执行
2
3
4
5
6
7
8
9
10
11
12
13
14
异常对象的常用方法:
try {
int[] arr = null;
System.out.println(arr[0]);
} catch (NullPointerException e) {
// getMessage():获取异常消息
System.out.println("消息:" + e.getMessage());
// toString():获取异常类型 + 消息
System.out.println("描述:" + e.toString());
// printStackTrace():打印完整的调用栈轨迹(调试用)
e.printStackTrace();
// getStackTrace():以数组形式获取调用栈
StackTraceElement[] stack = e.getStackTrace();
for (StackTraceElement element : stack) {
System.out.println(" at " + element);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 10.1.5 综合案例:异常信息分析器
本案例综合运用异常概念、异常体系、try-catch 基本语法、异常对象方法。
/**
* 异常信息分析器 —— 异常入门综合案例
* 知识点:异常体系、try-catch、getMessage/toString/printStackTrace/getStackTrace
*/
public class ExceptionAnalyzer {
// 模拟多层调用链(制造有深度的栈轨迹)
static void methodA() {
methodB();
}
static void methodB() {
methodC();
}
static void methodC() {
throw new ArithmeticException("除数不能为零");
}
// 分析并打印异常的详细信息
static void analyzeException(Exception e) {
System.out.println(" getMessage() : " + e.getMessage());
System.out.println(" toString() : " + e.toString());
System.out.println(" 异常类型 : " + e.getClass().getSimpleName());
// getStackTrace() 获取调用栈
StackTraceElement[] stack = e.getStackTrace();
System.out.println(" 调用栈深度 : " + stack.length);
for (int i = 0; i < Math.min(stack.length, 3); i++) {
System.out.printf(" [%d] %s.%s (行%d)%n",
i, stack[i].getClassName(), stack[i].getMethodName(),
stack[i].getLineNumber());
}
}
public static void main(String[] args) {
System.out.println("===== 异常信息分析器 =====\n");
// 1. 运行时异常(RuntimeException子类)
System.out.println("--- 测试1: ArithmeticException(调用链传播)---");
try {
methodA(); // A → B → C,在C中抛出异常
} catch (ArithmeticException e) {
analyzeException(e);
}
// 2. 空指针异常
System.out.println("\n--- 测试2: NullPointerException ---");
try {
String str = null;
str.length(); // 空指针
} catch (NullPointerException e) {
analyzeException(e);
}
// 3. 数组越界
System.out.println("\n--- 测试3: ArrayIndexOutOfBoundsException ---");
try {
int[] arr = {1, 2, 3};
int val = arr[10];
} catch (ArrayIndexOutOfBoundsException e) {
analyzeException(e);
}
// 4. 类型转换异常
System.out.println("\n--- 测试4: ClassCastException ---");
try {
Object obj = "hello";
Integer num = (Integer) obj;
} catch (ClassCastException e) {
analyzeException(e);
}
// 5. 异常后程序继续执行
System.out.println("\n--- 测试5: 异常不会终止程序 ---");
System.out.println(" try-catch 处理后程序正常继续 ✓");
// 6. 异常体系关系验证
System.out.println("\n--- 异常体系关系验证 ---");
ArithmeticException ae = new ArithmeticException();
System.out.println(" ArithmeticException instanceof RuntimeException : "
+ (ae instanceof RuntimeException));
System.out.println(" ArithmeticException instanceof Exception : "
+ (ae instanceof Exception));
System.out.println(" ArithmeticException instanceof Throwable : "
+ (ae instanceof Throwable));
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
运行结果:
===== 异常信息分析器 =====
--- 测试1: ArithmeticException(调用链传播)---
getMessage() : 除数不能为零
toString() : java.lang.ArithmeticException: 除数不能为零
异常类型 : ArithmeticException
调用栈深度 : 4
[0] ExceptionAnalyzer.methodC (行16)
[1] ExceptionAnalyzer.methodB (行12)
[2] ExceptionAnalyzer.methodA (行8)
--- 测试2: NullPointerException ---
getMessage() : null
toString() : java.lang.NullPointerException
异常类型 : NullPointerException
调用栈深度 : 1
[0] ExceptionAnalyzer.main (行34)
--- 测试3: ArrayIndexOutOfBoundsException ---
getMessage() : Index 10 out of bounds for length 3
异常类型 : ArrayIndexOutOfBoundsException
--- 测试4: ClassCastException ---
getMessage() : class java.lang.String cannot be cast to class java.lang.Integer
异常类型 : ClassCastException
--- 测试5: 异常不会终止程序 ---
try-catch 处理后程序正常继续 ✓
--- 异常体系关系验证 ---
ArithmeticException instanceof RuntimeException : true
ArithmeticException instanceof Exception : true
ArithmeticException instanceof Throwable : true
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
# 10.1.6 异常入门训练题
训练1:编写一个方法 int divide(int a, int b),当 b 为 0 时抛出异常。在 main 方法中调用并捕获异常,打印友好的错误信息。
训练2:编写程序,让用户输入一个整数。使用 try-catch 处理用户输入非数字字符串的情况(NumberFormatException)。
思考:以下代码中 System.out.println("B") 会执行吗?为什么?
try {
System.out.println("A");
int r = 10 / 0;
System.out.println("B");
} catch (ArithmeticException e) {
System.out.println("C");
}
System.out.println("D");
2
3
4
5
6
7
8
# 10.2 多级catch匹配
# 10.2.1 多级catch使用
try {
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // ArrayIndexOutOfBoundsException
System.out.println(10 / 0); // ArithmeticException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界:" + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("算术异常:" + e.getMessage());
} catch (Exception e) {
System.out.println("其他异常:" + e.getMessage());
}
2
3
4
5
6
7
8
9
10
11
注意:catch 的顺序必须从子类到父类,否则编译错误。
# 10.2.2 多异常捕获(JDK7+)
try {
// ...
} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
System.out.println("发生异常:" + e.getMessage());
}
2
3
4
5
# 10.2.3 综合案例:输入安全解析器
本案例综合运用多级 catch 匹配和 JDK7+ 多异常捕获语法。
/**
* 输入安全解析器 —— 多级catch综合案例
* 知识点:多级catch从子类到父类、JDK7+多异常捕获、catch顺序
*/
public class SafeParser {
// 安全解析整数
static int safeParseInt(String input) {
try {
if (input == null) {
throw new NullPointerException("输入为null");
}
return Integer.parseInt(input.trim());
} catch (NumberFormatException e) {
System.out.println(" 格式错误:\"" + input + "\" 不是有效整数");
return 0;
} catch (NullPointerException e) {
System.out.println(" 空值错误:" + e.getMessage());
return 0;
}
}
// 安全数组访问
static String safeArrayAccess(String[] arr, int index) {
try {
return arr[index].toUpperCase();
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
// JDK7+ 多异常捕获
System.out.println(" " + e.getClass().getSimpleName() + ": " + e.getMessage());
return "(默认值)";
}
}
// 安全除法(演示 catch 顺序:子类在前,父类在后)
static double safeDivide(String numStr, String denStr) {
try {
double num = Double.parseDouble(numStr);
double den = Double.parseDouble(denStr);
if (den == 0) throw new ArithmeticException("除数为零");
return num / den;
} catch (ArithmeticException e) {
System.out.println(" 算术错误:" + e.getMessage());
return 0;
} catch (NumberFormatException e) {
System.out.println(" 格式错误:" + e.getMessage());
return 0;
} catch (Exception e) {
// 兜底:必须放在最后
System.out.println(" 未知错误:" + e.getMessage());
return 0;
}
}
public static void main(String[] args) {
System.out.println("===== 输入安全解析器 =====\n");
// 1. 多级catch:整数解析
System.out.println("--- 安全解析整数 ---");
String[] inputs = {"42", "3.14", "abc", null, " 100 "};
for (String input : inputs) {
int result = safeParseInt(input);
System.out.println(" 解析 \"" + input + "\" → " + result);
}
// 2. JDK7+ 多异常捕获
System.out.println("\n--- 安全数组访问(多异常捕获)---");
String[] names = {"Java", null, "Python"};
System.out.println(" [0] = " + safeArrayAccess(names, 0));
System.out.println(" [1] = " + safeArrayAccess(names, 1)); // null元素
System.out.println(" [5] = " + safeArrayAccess(names, 5)); // 越界
// 3. catch 顺序:子类 → 父类
System.out.println("\n--- 安全除法(catch顺序)---");
System.out.printf(" 10 / 3 = %.2f%n", safeDivide("10", "3"));
System.out.printf(" 10 / 0 = %.2f%n", safeDivide("10", "0"));
System.out.printf(" abc / 2 = %.2f%n", safeDivide("abc", "2"));
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
运行结果:
===== 输入安全解析器 =====
--- 安全解析整数 ---
解析 "42" → 42
格式错误:"3.14" 不是有效整数
解析 "3.14" → 0
格式错误:"abc" 不是有效整数
解析 "abc" → 0
空值错误:输入为null
解析 "null" → 0
解析 " 100 " → 100
--- 安全数组访问(多异常捕获)---
[0] = JAVA
NullPointerException: null
[1] = (默认值)
ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3
[5] = (默认值)
--- 安全除法(catch顺序)---
10 / 3 = 3.33
算术错误:除数为零
10 / 0 = 0.00
格式错误:For input string: "abc"
abc / 2 = 0.00
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 10.2.4 多级catch训练题
训练1:编写一个方法,接收一个字符串数组,尝试将每个元素转换为整数并求和。使用多级 catch 分别处理 NumberFormatException、NullPointerException 和 Exception,打印每次异常的详细信息。
训练2:以下代码能编译通过吗?为什么?
try {
// ...
} catch (Exception e) {
System.out.println("父类");
} catch (ArithmeticException e) {
System.out.println("子类");
}
2
3
4
5
6
7
思考:JDK 7 的多异常捕获 catch (A | B e) 中,变量 e 是什么类型?能对 e 重新赋值吗?
# 10.3 finally语句
# 10.3.1 finally基本用法
finally 块中的代码无论是否发生异常都会执行,通常用于释放资源:
FileReader reader = null;
try {
reader = new FileReader("test.txt");
// 读取文件
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
} finally {
// 释放资源
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 10.3.2 try-with-resources(JDK7+)
JDK 7 引入了 try-with-resources,自动关闭资源:
// 自动关闭 reader,不需要 finally
try (FileReader reader = new FileReader("test.txt");
BufferedReader br = new BufferedReader(reader)) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("IO异常:" + e.getMessage());
}
// reader 和 br 会自动关闭
2
3
4
5
6
7
8
9
资源类必须实现
AutoCloseable接口。
# 10.3.3 综合案例:资源管理对比实验
本案例对比 finally 手动关闭和 try-with-resources 自动关闭,演示资源管理的最佳实践。
/**
* 资源管理对比实验 —— finally综合案例
* 知识点:finally执行时机、try-with-resources、AutoCloseable、Suppressed Exception
*/
class MyResource implements AutoCloseable {
private String name;
private boolean throwOnClose;
public MyResource(String name, boolean throwOnClose) {
this.name = name;
System.out.println(" [" + name + "] 资源打开");
}
public void use() {
System.out.println(" [" + name + "] 使用资源");
}
public void useWithError() {
System.out.println(" [" + name + "] 使用资源(将抛出异常)");
throw new RuntimeException(name + " 使用时发生错误");
}
@Override
public void close() {
System.out.println(" [" + name + "] 资源关闭");
if (throwOnClose) {
throw new RuntimeException(name + " 关闭时发生错误");
}
}
}
public class ResourceManagementDemo {
public static void main(String[] args) {
System.out.println("===== 资源管理对比实验 =====\n");
// 实验1:finally 手动关闭
System.out.println("--- 实验1: finally 手动关闭 ---");
MyResource res = null;
try {
res = new MyResource("手动资源", false);
res.use();
} catch (Exception e) {
System.out.println(" 捕获: " + e.getMessage());
} finally {
if (res != null) res.close(); // 手动关闭
System.out.println(" finally 执行完毕");
}
// 实验2:try-with-resources 自动关闭
System.out.println("\n--- 实验2: try-with-resources 自动关闭 ---");
try (MyResource r1 = new MyResource("自动资源A", false);
MyResource r2 = new MyResource("自动资源B", false)) {
r1.use();
r2.use();
}
System.out.println(" 注意:关闭顺序与打开顺序相反(B先关,A后关)");
// 实验3:finally 中 return 的陷阱
System.out.println("\n--- 实验3: finally 中 return ---");
System.out.println(" 结果: " + testFinallyReturn());
// 实验4:Suppressed Exception
System.out.println("\n--- 实验4: Suppressed Exception ---");
try (MyResource r = new MyResource("易碎资源", true)) {
r.useWithError(); // try 中抛异常
// close() 也会抛异常 → 成为 suppressed exception
} catch (Exception e) {
System.out.println(" 主异常: " + e.getMessage());
for (Throwable s : e.getSuppressed()) {
System.out.println(" 被抑制: " + s.getMessage());
}
}
}
static int testFinallyReturn() {
try {
System.out.println(" try 块执行");
return 1;
} finally {
System.out.println(" finally 块执行");
// return 2; // 警告:finally 中的 return 会覆盖 try 中的 return!
}
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
运行结果:
===== 资源管理对比实验 =====
--- 实验1: finally 手动关闭 ---
[手动资源] 资源打开
[手动资源] 使用资源
[手动资源] 资源关闭
finally 执行完毕
--- 实验2: try-with-resources 自动关闭 ---
[自动资源A] 资源打开
[自动资源B] 资源打开
[自动资源A] 使用资源
[自动资源B] 使用资源
[自动资源B] 资源关闭
[自动资源A] 资源关闭
注意:关闭顺序与打开顺序相反(B先关,A后关)
--- 实验3: finally 中 return ---
try 块执行
finally 块执行
结果: 1
--- 实验4: Suppressed Exception ---
[易碎资源] 资源打开
[易碎资源] 使用资源(将抛出异常)
[易碎资源] 资源关闭
主异常: 易碎资源 使用时发生错误
被抑制: 易碎资源 关闭时发生错误
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
# 10.3.4 finally和try-with-resources训练题
训练1:以下代码的输出是什么?请解释执行顺序:
public static int test() {
try {
System.out.println("try");
return 1;
} catch (Exception e) {
System.out.println("catch");
return 2;
} finally {
System.out.println("finally");
}
}
2
3
4
5
6
7
8
9
10
11
训练2:编写一个实现 AutoCloseable 接口的 MyResource 类,在构造方法中打印"资源打开",在 close() 中打印"资源关闭"。用 try-with-resources 创建并使用它,观察输出顺序。
思考:try-with-resources 中如果 try 块抛出异常,close() 也抛出异常,最终抛出的是哪个?另一个去哪了?(提示:Suppressed Exception)
# 10.4 throw和throws
# 10.4.1 throw抛出异常
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("存款金额必须大于0");
}
balance += amount;
}
2
3
4
5
6
# 10.4.2 throws声明异常
throws 用于在方法签名中声明该方法可能抛出的异常:
// 声明可能抛出 IOException
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
// ...
}
// 调用方必须处理异常
public void process() {
try {
readFile("test.txt");
} catch (IOException e) {
System.out.println("文件读取失败");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
对比 C++:C++ 的异常规范(throw())已被弃用,C++11 引入了 noexcept。Java 的 throws 是强制性的(对于检查异常),这使得异常处理更加规范。
# 10.4.3 综合案例:参数校验工具
本案例综合运用 throw 抛出异常和 throws 声明异常,展示异常在方法调用链中的传播。
/**
* 参数校验工具 —— throw/throws 综合案例
* 知识点:throw抛出、throws声明、检查异常vs运行时异常、异常传播
*/
public class ParamValidator {
// throw 运行时异常:调用方可以不处理
static void validateNotNull(Object obj, String name) {
if (obj == null) {
throw new IllegalArgumentException(name + " 不能为null");
}
}
// throw 运行时异常:范围校验
static void validateRange(int value, int min, int max, String name) {
if (value < min || value > max) {
throw new IllegalArgumentException(
String.format("%s 超出范围[%d, %d],实际值: %d", name, min, max, value));
}
}
// throws 检查异常:调用方必须处理
static String validateEmail(String email) throws Exception {
validateNotNull(email, "email");
if (!email.contains("@") || !email.contains(".")) {
throw new Exception("邮箱格式无效: " + email);
}
return email.toLowerCase().trim();
}
// 异常链传播:底层 → 中层 → 顶层
static void registerUser(String name, int age, String email)
throws Exception {
System.out.println(" 注册用户: " + name);
validateNotNull(name, "用户名"); // 可能抛运行时异常
validateRange(age, 0, 150, "年龄"); // 可能抛运行时异常
String validEmail = validateEmail(email); // 可能抛检查异常
System.out.println(" ✓ 注册成功: " + name + ", " + age + "岁, " + validEmail);
}
public static void main(String[] args) {
System.out.println("===== 参数校验工具 =====\n");
// 测试1:正常注册
System.out.println("--- 测试1: 正常注册 ---");
try {
registerUser("张三", 25, "zhangsan@mail.com");
} catch (Exception e) {
System.out.println(" ✗ " + e.getMessage());
}
// 测试2:null 参数(运行时异常)
System.out.println("\n--- 测试2: null参数 ---");
try {
registerUser(null, 25, "test@mail.com");
} catch (IllegalArgumentException e) {
System.out.println(" ✗ 运行时异常: " + e.getMessage());
} catch (Exception e) {
System.out.println(" ✗ 检查异常: " + e.getMessage());
}
// 测试3:年龄超出范围(运行时异常)
System.out.println("\n--- 测试3: 年龄超出范围 ---");
try {
registerUser("李四", 200, "lisi@mail.com");
} catch (IllegalArgumentException e) {
System.out.println(" ✗ 运行时异常: " + e.getMessage());
} catch (Exception e) {
System.out.println(" ✗ 检查异常: " + e.getMessage());
}
// 测试4:邮箱无效(检查异常)
System.out.println("\n--- 测试4: 邮箱无效 ---");
try {
registerUser("王五", 30, "invalid-email");
} catch (IllegalArgumentException e) {
System.out.println(" ✗ 运行时异常: " + e.getMessage());
} catch (Exception e) {
System.out.println(" ✗ 检查异常: " + e.getMessage());
}
// 对比:throw vs throws
System.out.println("\n--- throw vs throws 对比 ---");
System.out.println(" throw : 在方法体内抛出异常对象");
System.out.println(" throws : 在方法签名上声明可能的异常类型");
System.out.println(" 运行时异常: 不需要 throws 声明");
System.out.println(" 检查异常 : 必须 throws 声明或 try-catch");
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
运行结果:
===== 参数校验工具 =====
--- 测试1: 正常注册 ---
注册用户: 张三
✓ 注册成功: 张三, 25岁, zhangsan@mail.com
--- 测试2: null参数 ---
注册用户: null
✗ 运行时异常: 用户名 不能为null
--- 测试3: 年龄超出范围 ---
注册用户: 李四
✗ 运行时异常: 年龄 超出范围[0, 150],实际值: 200
--- 测试4: 邮箱无效 ---
注册用户: 王五
✗ 检查异常: 邮箱格式无效: invalid-email
--- throw vs throws 对比 ---
throw : 在方法体内抛出异常对象
throws : 在方法签名上声明可能的异常类型
运行时异常: 不需要 throws 声明
检查异常 : 必须 throws 声明或 try-catch
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 10.4.4 throw和throws训练题
训练1:编写一个方法 int parseInt(String s),如果 s 为 null 抛出 IllegalArgumentException,如果 s 不是合法整数抛出 NumberFormatException。在 main 方法中测试不同的输入。
训练2:以下两种写法有什么区别?什么时候选择哪种?
// 写法1:try-catch 处理
public void method1() {
try { riskyOperation(); }
catch (IOException e) { e.printStackTrace(); }
}
// 写法2:throws 声明
public void method2() throws IOException {
riskyOperation();
}
2
3
4
5
6
7
8
9
10
思考:如果一个方法 throws 了检查异常,所有调用它的方法都必须处理(try-catch 或继续 throws)。这种"异常传播"在大型项目中会导致什么问题?Spring 为什么将大部分检查异常包装成运行时异常?
# 10.5 自定义异常
# 10.5.1 自定义异常类
// 继承 Exception:检查异常(调用方必须处理)
public class InsufficientBalanceException extends Exception {
private double balance;
private double amount;
public InsufficientBalanceException(double balance, double amount) {
super("余额不足:当前余额 " + balance + ",尝试取款 " + amount);
this.balance = balance;
this.amount = amount;
}
public double getBalance() { return balance; }
public double getAmount() { return amount; }
}
// 继承 RuntimeException:运行时异常(调用方可以不处理)
public class InvalidAccountException extends RuntimeException {
public InvalidAccountException(String message) {
super(message);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 10.5.2 自定义异常实践
public class Account {
private String name;
private double balance;
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException(balance, amount);
}
balance -= amount;
}
}
// 使用
public class Main {
public static void main(String[] args) {
Account acc = new Account("张三", 1000);
try {
acc.withdraw(2000);
} catch (InsufficientBalanceException e) {
System.out.println(e.getMessage());
System.out.println("当前余额:" + e.getBalance());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 10.5.3 综合案例:订单异常体系
本案例综合运用自定义异常类设计,展示检查异常和运行时异常的选择。
/**
* 订单异常体系 —— 自定义异常综合案例
* 知识点:自定义异常继承关系、携带业务信息、异常链
*/
// 自定义异常基类(检查异常)
class OrderException extends Exception {
private final String orderId;
public OrderException(String orderId, String message) {
super(message);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
// 库存不足异常
class StockException extends OrderException {
private final String product;
private final int requested;
private final int available;
public StockException(String orderId, String product, int requested, int available) {
super(orderId, String.format("库存不足[%s]:需要%d,仅剩%d", product, requested, available));
this.product = product;
this.requested = requested;
this.available = available;
}
public int getShortage() { return requested - available; }
}
// 支付失败异常
class PaymentException extends OrderException {
public PaymentException(String orderId, String reason) {
super(orderId, "支付失败: " + reason);
}
}
// 非法订单异常(运行时异常:程序bug)
class IllegalOrderException extends RuntimeException {
public IllegalOrderException(String message) {
super(message);
}
}
// 简单订单系统
class OrderService {
static void createOrder(String orderId, String product, int qty, double payment)
throws OrderException {
// 参数校验(运行时异常)
if (orderId == null || orderId.isEmpty()) {
throw new IllegalOrderException("订单ID不能为空");
}
// 库存检查(检查异常)
int stock = 10; // 模拟库存
if (qty > stock) {
throw new StockException(orderId, product, qty, stock);
}
// 支付验证(检查异常)
double price = qty * 99.9;
if (payment < price) {
throw new PaymentException(orderId,
String.format("金额不足,需要%.2f,支付%.2f", price, payment));
}
System.out.printf(" ✓ 订单[%s] %s×%d 创建成功,支付%.2f%n",
orderId, product, qty, payment);
}
}
public class OrderExceptionDemo {
public static void main(String[] args) {
System.out.println("===== 订单异常体系演示 =====\n");
String[][] tests = {
{"ORD001", "Java教程", "3", "300"}, // 正常
{"ORD002", "Java教程", "20", "2000"}, // 库存不足
{"ORD003", "Java教程", "5", "100"}, // 支付不足
{"", "Java教程", "1", "100"}, // 非法订单ID
};
for (String[] t : tests) {
System.out.println("--- 订单: " + t[0] + " ---");
try {
OrderService.createOrder(t[0], t[1],
Integer.parseInt(t[2]), Double.parseDouble(t[3]));
} catch (StockException e) {
System.out.println(" ✗ [库存异常] " + e.getMessage());
System.out.println(" 订单号: " + e.getOrderId() + ",还差: " + e.getShortage());
} catch (PaymentException e) {
System.out.println(" ✗ [支付异常] " + e.getMessage());
System.out.println(" 订单号: " + e.getOrderId());
} catch (OrderException e) {
System.out.println(" ✗ [订单异常] " + e.getMessage());
} catch (IllegalOrderException e) {
System.out.println(" ✗ [程序错误] " + e.getMessage());
}
}
// 异常继承关系验证
System.out.println("\n--- 异常继承关系 ---");
StockException se = new StockException("X", "Y", 1, 0);
System.out.println(" StockException instanceof OrderException : " + (se instanceof OrderException));
System.out.println(" StockException instanceof Exception : " + (se instanceof Exception));
System.out.println(" 选择建议:业务异常→检查异常,程序bug→运行时异常");
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
运行结果:
===== 订单异常体系演示 =====
--- 订单: ORD001 ---
✓ 订单[ORD001] Java教程×3 创建成功,支付300.00
--- 订单: ORD002 ---
✗ [库存异常] 库存不足[Java教程]:需要20,仅剩10
订单号: ORD002,还差: 10
--- 订单: ORD003 ---
✗ [支付异常] 支付失败: 金额不足,需要499.50,支付100.00
订单号: ORD003
--- 订单: ---
✗ [程序错误] 订单ID不能为空
--- 异常继承关系 ---
StockException instanceof OrderException : true
StockException instanceof Exception : true
选择建议:业务异常→检查异常,程序bug→运行时异常
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 10.5.4 自定义异常训练题
训练1:设计一套电商系统的异常体系:
OrderException(订单异常基类,检查异常)OrderNotFoundException extends OrderExceptionPaymentFailedException extends OrderExceptionStockInsufficientException extends OrderException
为每个异常类添加有意义的属性(如订单号、商品名、库存数),编写代码模拟下单流程并捕获不同的异常。
训练2:自定义异常应该继承 Exception 还是 RuntimeException?请根据以下场景给出选择并说明理由:
- (a) 用户密码错误
- (b) 数据库连接失败
- (c) 程序内部逻辑 bug(如传入 null 参数)
- (d) 外部 API 返回错误
疑惑:为什么 Spring 框架把几乎所有异常都定义为 RuntimeException?
答疑:检查异常(Checked Exception)要求每个调用方都必须处理,在大型项目中会导致"异常签名污染"——低层的 SQLException 一路 throws 到最上层,破坏了分层架构的封装性。Spring 的理念是:如果调用方无法有效处理异常(如数据库连接失败),强制要求处理只会产生大量 catch (Exception e) { e.printStackTrace(); } 的无效代码。
论证:
// 检查异常的问题:异常签名层层传播
// DAO → Service → Controller,每一层都要 throws
public User findUser(int id) throws SQLException { ... } // DAO
public User getUser(int id) throws SQLException { ... } // Service
public Response handleRequest(int id) throws SQLException { ... } // Controller
// Spring 的做法:包装为运行时异常
public User findUser(int id) {
try {
return jdbcTemplate.queryForObject(...);
} catch (DataAccessException e) { // RuntimeException
// Spring 把 SQLException 包装为 DataAccessException
throw e;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
结果展示:Kotlin、C# 都没有检查异常,Go 使用错误值而非异常。Java 的检查异常是一个有争议的设计决策。
# 10.6 常见异常汇总
| 异常 | 含义 | 常见场景 |
|---|---|---|
NullPointerException | 空指针异常 | 对 null 对象调用方法 |
ArrayIndexOutOfBoundsException | 数组越界 | 访问超出数组范围的索引 |
ArithmeticException | 算术异常 | 整数除以0 |
ClassCastException | 类型转换异常 | 强制转换类型不匹配 |
NumberFormatException | 数字格式异常 | 字符串转数字失败 |
IllegalArgumentException | 非法参数 | 方法参数不合法 |
IOException | IO异常 | 文件读写失败 |
FileNotFoundException | 文件未找到 | 指定文件不存在 |
StackOverflowError | 栈溢出 | 递归太深 |
OutOfMemoryError | 内存溢出 | 堆内存不够 |
# 10.6.1 综合案例:异常速查手册
本案例汇总演示各种常见异常的触发和处理方式。
/**
* 异常速查手册 —— 常见异常综合案例
* 一次性演示 8 种常见异常的触发和处理
*/
public class ExceptionQuickRef {
static void testException(String name, Runnable action) {
System.out.printf("%-35s", " " + name + ": ");
try {
action.run();
System.out.println("✓ 正常");
} catch (Exception e) {
System.out.println("✗ " + e.getClass().getSimpleName()
+ " → " + e.getMessage());
} catch (Error e) {
System.out.println("✗ " + e.getClass().getSimpleName()
+ " → " + e.getMessage());
}
}
static int recursion(int n) { return recursion(n + 1); }
public static void main(String[] args) {
System.out.println("===== 常见异常速查手册 =====\n");
// 1. NullPointerException
testException("NullPointerException",
() -> { String s = null; s.length(); });
// 2. ArrayIndexOutOfBoundsException
testException("ArrayIndexOutOfBoundsException",
() -> { int[] a = {1}; int x = a[5]; });
// 3. ArithmeticException
testException("ArithmeticException",
() -> { int x = 10 / 0; });
// 4. ClassCastException
testException("ClassCastException",
() -> { Object o = "abc"; Integer i = (Integer) o; });
// 5. NumberFormatException
testException("NumberFormatException",
() -> Integer.parseInt("abc"));
// 6. IllegalArgumentException
testException("IllegalArgumentException",
() -> { throw new IllegalArgumentException("参数不合法"); });
// 7. StringIndexOutOfBoundsException
testException("StringIndexOutOfBoundsException",
() -> "abc".charAt(10));
// 8. StackOverflowError
testException("StackOverflowError",
() -> recursion(0));
System.out.println("\n--- 异常分类总结 ---");
System.out.println(" 运行时异常(RuntimeException): 编译器不强制处理");
System.out.println(" → NPE, 数组越界, 算术异常, 类型转换, 数字格式");
System.out.println(" 检查异常(Checked Exception): 编译器强制处理");
System.out.println(" → IOException, SQLException, FileNotFoundException");
System.out.println(" 错误(Error): 严重问题,一般不捕获");
System.out.println(" → StackOverflowError, OutOfMemoryError");
}
}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
运行结果:
===== 常见异常速查手册 =====
NullPointerException : ✗ NullPointerException → null
ArrayIndexOutOfBoundsException : ✗ ArrayIndexOutOfBoundsException → Index 5 out of bounds for length 1
ArithmeticException : ✗ ArithmeticException → / by zero
ClassCastException : ✗ ClassCastException → class java.lang.String cannot be cast to class java.lang.Integer
NumberFormatException : ✗ NumberFormatException → For input string: "abc"
IllegalArgumentException : ✗ IllegalArgumentException → 参数不合法
StringIndexOutOfBoundsException : ✗ StringIndexOutOfBoundsException → String index out of range: 10
StackOverflowError : ✗ StackOverflowError → null
--- 异常分类总结 ---
运行时异常(RuntimeException): 编译器不强制处理
→ NPE, 数组越界, 算术异常, 类型转换, 数字格式
检查异常(Checked Exception): 编译器强制处理
→ IOException, SQLException, FileNotFoundException
错误(Error): 严重问题,一般不捕获
→ StackOverflowError, OutOfMemoryError
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 10.6.2 常见异常训练题
训练1:编写代码分别触发以下异常,并用 try-catch 捕获并打印友好信息:NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException、NumberFormatException。
训练2:Error 和 Exception 有什么区别?StackOverflowError 可以被 try-catch 捕获吗?如果可以,捕获后程序能正常运行吗?
思考:为什么 NullPointerException 是运行时异常而不是检查异常?如果它是检查异常,Java 代码会变成什么样?