|
@@ -2,7 +2,7 @@
|
|
|
|
|
|
|
|
> **说明**:以下习题围绕今日授课核心内容——**Java 异常处理(Exception Handling)** 展开,涵盖 Throwable 体系结构、try-catch 多 catch 块捕获处理、异常常用方法(getMessage/printStackTrace/getCause)、常见运行时异常(数组下标越界、空指针、类型转换、算术异常等)、throw 与 throws 关键字、自定义异常、异常处理的鲁棒性等知识点。
|
|
> **说明**:以下习题围绕今日授课核心内容——**Java 异常处理(Exception Handling)** 展开,涵盖 Throwable 体系结构、try-catch 多 catch 块捕获处理、异常常用方法(getMessage/printStackTrace/getCause)、常见运行时异常(数组下标越界、空指针、类型转换、算术异常等)、throw 与 throws 关键字、自定义异常、异常处理的鲁棒性等知识点。
|
|
|
>
|
|
>
|
|
|
-> 题目分为两部分:**随堂练习**(异常处理基础,8 题)和 **进阶练习**(综合挑战,3 题)。
|
|
|
|
|
|
|
+> 题目分为两部分:**随堂练习**(异常处理基础,8 题)和 **进阶练习**(综合挑战,4 题)。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
@@ -788,11 +788,14 @@ public class RegisterService {
|
|
|
*/
|
|
*/
|
|
|
public static void register(String username, String password, String age) {
|
|
public static void register(String username, String password, String age) {
|
|
|
// 要求:
|
|
// 要求:
|
|
|
- // 1. 使用 try-catch 调用各验证方法
|
|
|
|
|
|
|
+ // 1. 使用 try-catch-finally 调用各验证方法
|
|
|
// 2. 按顺序验证:用户名 → 密码 → 年龄
|
|
// 2. 按顺序验证:用户名 → 密码 → 年龄
|
|
|
// 3. 每个验证都有独立的 catch 块,输出对应的错误提示
|
|
// 3. 每个验证都有独立的 catch 块,输出对应的错误提示
|
|
|
// 4. 如果所有验证都通过,输出:"注册成功!欢迎 " + username
|
|
// 4. 如果所有验证都通过,输出:"注册成功!欢迎 " + username
|
|
|
// 5. 注意:validateAge() 可能抛出 NumberFormatException,也需要捕获
|
|
// 5. 注意:validateAge() 可能抛出 NumberFormatException,也需要捕获
|
|
|
|
|
+ // 6. 【新增】添加 finally 块,无论注册成功还是失败,都输出:
|
|
|
|
|
+ // "—— 注册操作日志:用户名=" + username + ",结果=" + (是否成功 ? "成功" : "失败")
|
|
|
|
|
+ // 提示:可以在 try 块成功时用一个 boolean 变量标记成功,finally 中读取该变量
|
|
|
|
|
|
|
|
// 请补充代码
|
|
// 请补充代码
|
|
|
}
|
|
}
|
|
@@ -826,32 +829,40 @@ public class RegisterService {
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 预期输出
|
|
|
|
|
|
|
+### 预期输出(含 finally 操作日志)
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
========== 测试 1:正常注册 ==========
|
|
========== 测试 1:正常注册 ==========
|
|
|
注册成功!欢迎 张三丰
|
|
注册成功!欢迎 张三丰
|
|
|
|
|
+—— 注册操作日志:用户名=张三丰,结果=成功
|
|
|
|
|
|
|
|
========== 测试 2:用户名为空 ==========
|
|
========== 测试 2:用户名为空 ==========
|
|
|
用户名不能为空!
|
|
用户名不能为空!
|
|
|
|
|
+—— 注册操作日志:用户名=,结果=失败
|
|
|
|
|
|
|
|
========== 测试 3:用户名太短 ==========
|
|
========== 测试 3:用户名太短 ==========
|
|
|
用户名长度必须在 3~12 个字符之间!
|
|
用户名长度必须在 3~12 个字符之间!
|
|
|
|
|
+—— 注册操作日志:用户名=12,结果=失败
|
|
|
|
|
|
|
|
========== 测试 4:密码太短 ==========
|
|
========== 测试 4:密码太短 ==========
|
|
|
密码长度不能少于 6 位!
|
|
密码长度不能少于 6 位!
|
|
|
|
|
+—— 注册操作日志:用户名=张三丰,结果=失败
|
|
|
|
|
|
|
|
========== 测试 5:密码不包含数字 ==========
|
|
========== 测试 5:密码不包含数字 ==========
|
|
|
密码必须包含数字和字母!
|
|
密码必须包含数字和字母!
|
|
|
|
|
+—— 注册操作日志:用户名=张三丰,结果=失败
|
|
|
|
|
|
|
|
========== 测试 6:年龄不是数字 ==========
|
|
========== 测试 6:年龄不是数字 ==========
|
|
|
年龄格式错误:二十五 不是有效的数字!
|
|
年龄格式错误:二十五 不是有效的数字!
|
|
|
|
|
+—— 注册操作日志:用户名=张三丰,结果=失败
|
|
|
|
|
|
|
|
========== 测试 7:年龄超出范围 ==========
|
|
========== 测试 7:年龄超出范围 ==========
|
|
|
年龄必须在 0~150 之间!
|
|
年龄必须在 0~150 之间!
|
|
|
|
|
+—— 注册操作日志:用户名=张三丰,结果=失败
|
|
|
|
|
|
|
|
========== 测试 8:综合错误 ==========
|
|
========== 测试 8:综合错误 ==========
|
|
|
用户名不能为空!
|
|
用户名不能为空!
|
|
|
|
|
+—— 注册操作日志:用户名=,结果=失败
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
### 思考题
|
|
### 思考题
|
|
@@ -860,6 +871,8 @@ public class RegisterService {
|
|
|
2. 为什么 `register()` 方法中,每个验证方法都有自己的 catch 块,而不是用一个大 `catch (Exception e)` 统一处理?这样做的好处是什么?
|
|
2. 为什么 `register()` 方法中,每个验证方法都有自己的 catch 块,而不是用一个大 `catch (Exception e)` 统一处理?这样做的好处是什么?
|
|
|
3. 测试 8 中,用户名、密码、年龄同时出错,为什么只报了用户名的错误?这体现了什么设计思想?
|
|
3. 测试 8 中,用户名、密码、年龄同时出错,为什么只报了用户名的错误?这体现了什么设计思想?
|
|
|
4. 自定义异常在实际项目中有哪些应用场景?什么时候应该自定义异常而不是使用 Java 内置异常?
|
|
4. 自定义异常在实际项目中有哪些应用场景?什么时候应该自定义异常而不是使用 Java 内置异常?
|
|
|
|
|
+5. **【finally 思考】** 本练习中 `register()` 方法使用 `finally` 输出了操作日志。如果不用 `finally`,把日志输出放在 try 块末尾和每个 catch 块中分别写一次,是否能达到同样效果?这样做有什么弊端?(提示:代码重复、遗漏风险)
|
|
|
|
|
+6. **【finally 思考】** 如果 try 块中有 `return` 语句,`finally` 块还会执行吗?请编写一个小测试验证你的结论。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
@@ -1018,14 +1031,16 @@ public class ATM {
|
|
|
System.out.println("\n【" + location + " ATM】取款操作:账号 " + accountId + ",金额 " + amount + " 元");
|
|
System.out.println("\n【" + location + " ATM】取款操作:账号 " + accountId + ",金额 " + amount + " 元");
|
|
|
|
|
|
|
|
// 要求:
|
|
// 要求:
|
|
|
- // 1. 使用 try-catch 调用 findAccount() 查找账户
|
|
|
|
|
|
|
+ // 1. 使用 try-catch-finally 调用 findAccount() 查找账户
|
|
|
// 2. 找到账户后,调用 account.withdraw(amount) 执行取款
|
|
// 2. 找到账户后,调用 account.withdraw(amount) 执行取款
|
|
|
// 3. 捕获以下异常并给出提示:
|
|
// 3. 捕获以下异常并给出提示:
|
|
|
// - AccountNotFoundException:输出 "取款失败:账号不存在!"
|
|
// - AccountNotFoundException:输出 "取款失败:账号不存在!"
|
|
|
// - InvalidAmountException:输出 "取款失败:金额不合法!" + e.getMessage()
|
|
// - InvalidAmountException:输出 "取款失败:金额不合法!" + e.getMessage()
|
|
|
// - WithdrawLimitExceededException:输出 "取款失败:超过限额!" + e.getMessage()
|
|
// - WithdrawLimitExceededException:输出 "取款失败:超过限额!" + e.getMessage()
|
|
|
// - InsufficientBalanceException:输出 "取款失败:余额不足!" + e.getMessage()
|
|
// - InsufficientBalanceException:输出 "取款失败:余额不足!" + e.getMessage()
|
|
|
- // 4. 无论是否发生异常,最后输出:"感谢使用 " + location + " ATM 服务!"
|
|
|
|
|
|
|
+ // 4. 【新增】在 finally 块中输出交易记录:
|
|
|
|
|
+ // "【交易记录】" + location + " | 取款 | 账号 " + accountId + " | 金额 " + amount + " 元"
|
|
|
|
|
+ // 5. finally 块之后输出:"感谢使用 " + location + " ATM 服务!"
|
|
|
// 请补充代码
|
|
// 请补充代码
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1037,14 +1052,14 @@ public class ATM {
|
|
|
public void deposit(String accountId, double amount) {
|
|
public void deposit(String accountId, double amount) {
|
|
|
System.out.println("\n【" + location + " ATM】存款操作:账号 " + accountId + ",金额 " + amount + " 元");
|
|
System.out.println("\n【" + location + " ATM】存款操作:账号 " + accountId + ",金额 " + amount + " 元");
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- Account account = findAccount(accountId);
|
|
|
|
|
- account.deposit(amount);
|
|
|
|
|
- } catch (AccountNotFoundException e) {
|
|
|
|
|
- System.out.println("存款失败:" + e.getMessage());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- System.out.println("感谢使用 " + location + " ATM 服务!");
|
|
|
|
|
|
|
+ // 要求:
|
|
|
|
|
+ // 1. 使用 try-catch-finally 调用 findAccount() 查找账户
|
|
|
|
|
+ // 2. 找到账户后,调用 account.deposit(amount) 执行存款
|
|
|
|
|
+ // 3. 捕获 AccountNotFoundException,输出:"存款失败:" + e.getMessage()
|
|
|
|
|
+ // 4. 【新增】在 finally 块中输出交易记录:
|
|
|
|
|
+ // "【交易记录】" + location + " | 存款 | 账号 " + accountId + " | 金额 " + amount + " 元"
|
|
|
|
|
+ // 5. finally 块之后输出:"感谢使用 " + location + " ATM 服务!"
|
|
|
|
|
+ // 请补充代码
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1118,7 +1133,7 @@ public class ATMTest {
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 预期输出(参考)
|
|
|
|
|
|
|
+### 预期输出(含 finally 交易记录)
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
账户 A001 已添加到 幸福路支行 ATM 系统
|
|
账户 A001 已添加到 幸福路支行 ATM 系统
|
|
@@ -1129,36 +1144,42 @@ public class ATMTest {
|
|
|
|
|
|
|
|
【幸福路支行 ATM】取款操作:账号 A001,金额 2000.0 元
|
|
【幸福路支行 ATM】取款操作:账号 A001,金额 2000.0 元
|
|
|
张三 取出 2000.0 元,剩余余额:8000.0 元
|
|
张三 取出 2000.0 元,剩余余额:8000.0 元
|
|
|
|
|
+【交易记录】幸福路支行 | 取款 | 账号 A001 | 金额 2000.0 元
|
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
|
|
|
|
|
|
========== 余额不足 ==========
|
|
========== 余额不足 ==========
|
|
|
|
|
|
|
|
【幸福路支行 ATM】取款操作:账号 A003,金额 3000.0 元
|
|
【幸福路支行 ATM】取款操作:账号 A003,金额 3000.0 元
|
|
|
取款失败:余额不足!当前余额:2000.0 元,需要:3000.0 元
|
|
取款失败:余额不足!当前余额:2000.0 元,需要:3000.0 元
|
|
|
|
|
+【交易记录】幸福路支行 | 取款 | 账号 A003 | 金额 3000.0 元
|
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
|
|
|
|
|
|
========== 超过限额 ==========
|
|
========== 超过限额 ==========
|
|
|
|
|
|
|
|
【幸福路支行 ATM】取款操作:账号 A001,金额 6000.0 元
|
|
【幸福路支行 ATM】取款操作:账号 A001,金额 6000.0 元
|
|
|
取款失败:超过限额!单次取款金额不能超过 5000 元!
|
|
取款失败:超过限额!单次取款金额不能超过 5000 元!
|
|
|
|
|
+【交易记录】幸福路支行 | 取款 | 账号 A001 | 金额 6000.0 元
|
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
|
|
|
|
|
|
========== 金额不合法 ==========
|
|
========== 金额不合法 ==========
|
|
|
|
|
|
|
|
【幸福路支行 ATM】取款操作:账号 A002,金额 123.0 元
|
|
【幸福路支行 ATM】取款操作:账号 A002,金额 123.0 元
|
|
|
取款失败:金额不合法!取款金额必须为正数且为 100 的倍数!
|
|
取款失败:金额不合法!取款金额必须为正数且为 100 的倍数!
|
|
|
|
|
+【交易记录】幸福路支行 | 取款 | 账号 A002 | 金额 123.0 元
|
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
|
|
|
|
|
|
========== 账户不存在 ==========
|
|
========== 账户不存在 ==========
|
|
|
|
|
|
|
|
【幸福路支行 ATM】取款操作:账号 A999,金额 1000.0 元
|
|
【幸福路支行 ATM】取款操作:账号 A999,金额 1000.0 元
|
|
|
取款失败:账号不存在!
|
|
取款失败:账号不存在!
|
|
|
|
|
+【交易记录】幸福路支行 | 取款 | 账号 A999 | 金额 1000.0 元
|
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
|
|
|
|
|
|
========== 存款 ==========
|
|
========== 存款 ==========
|
|
|
|
|
|
|
|
【幸福路支行 ATM】存款操作:账号 A002,金额 3000.0 元
|
|
【幸福路支行 ATM】存款操作:账号 A002,金额 3000.0 元
|
|
|
李四 存入 3000.0 元,余额:8000.0 元
|
|
李四 存入 3000.0 元,余额:8000.0 元
|
|
|
|
|
+【交易记录】幸福路支行 | 存款 | 账号 A002 | 金额 3000.0 元
|
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
感谢使用 幸福路支行 ATM 服务!
|
|
|
|
|
|
|
|
========== 查询余额 ==========
|
|
========== 查询余额 ==========
|
|
@@ -1203,6 +1224,9 @@ public void withdraw(double amount)
|
|
|
3. 本练习中的业务异常(如 `InsufficientBalanceException`)继承了 `Exception`(编译期异常),如果改为继承 `RuntimeException`,`ATM.withdraw()` 中的 catch 能不能省略?省略后会有什么问题?
|
|
3. 本练习中的业务异常(如 `InsufficientBalanceException`)继承了 `Exception`(编译期异常),如果改为继承 `RuntimeException`,`ATM.withdraw()` 中的 catch 能不能省略?省略后会有什么问题?
|
|
|
4. 在实际的银行系统中,除了本练习提到的异常,还可能有哪些业务异常?请列举至少 3 个。
|
|
4. 在实际的银行系统中,除了本练习提到的异常,还可能有哪些业务异常?请列举至少 3 个。
|
|
|
5. `findAccount()` 方法为什么设计为 `private`?为什么抛出 `AccountNotFoundException` 而不是返回 `null`?用异常和返回 null 各有什么优缺点?
|
|
5. `findAccount()` 方法为什么设计为 `private`?为什么抛出 `AccountNotFoundException` 而不是返回 `null`?用异常和返回 null 各有什么优缺点?
|
|
|
|
|
+6. **【finally 思考】** `ATM.withdraw()` 中的 `【交易记录】` 输出放在了 `finally` 块中。为什么不能放在 try 块的最末尾?如果取款成功时一切正常,交易记录当然会输出;但如果发生异常(如余额不足),try 块中哪一行代码之后的内容都不会执行了,交易记录还能输出吗?`finally` 在这里解决了什么问题?
|
|
|
|
|
+7. **【finally 思考】** 假设银行要求「每笔交易必须记录日志,即使系统崩溃也要先写日志再崩溃」。`finally` 块能否满足这个要求?为什么?(提示:如果在 finally 块中再次抛出异常会怎样?)
|
|
|
|
|
+8. **【代码调试】** 请在 `ATM.withdraw()` 的 try 块末尾添加一句 `System.out.println("取款操作完成");`,然后在 catch 块和 finally 块中也各添加一句输出。观察在「余额不足」的情况下,try、catch、finally 中的输出语句的执行顺序,说明异常发生时这三部分的执行流程。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
@@ -1357,23 +1381,31 @@ public class InventoryManager {
|
|
|
*/
|
|
*/
|
|
|
public void outbound(String productId, int quantity)
|
|
public void outbound(String productId, int quantity)
|
|
|
throws ProductNotFoundException, StockNotEnoughException {
|
|
throws ProductNotFoundException, StockNotEnoughException {
|
|
|
- // 1. 调用 findProduct() 查找商品(会抛出 ProductNotFoundException)
|
|
|
|
|
- Product product = findProduct(productId);
|
|
|
|
|
-
|
|
|
|
|
- // 2. 检查库存是否充足
|
|
|
|
|
- // 如果 product.getStock() < quantity,抛出 StockNotEnoughException
|
|
|
|
|
- // 提示信息:"商品 " + product.getName() + " 库存不足!当前库存:"
|
|
|
|
|
- // + product.getStock() + ",需要:" + quantity
|
|
|
|
|
- // 请补充代码
|
|
|
|
|
-
|
|
|
|
|
- // 3. 减少库存
|
|
|
|
|
- product.setStock(product.getStock() - quantity);
|
|
|
|
|
- System.out.println("出库成功:" + product.getName() + " × " + quantity
|
|
|
|
|
- + ",剩余库存:" + product.getStock());
|
|
|
|
|
|
|
+ // 注意:此方法使用 try-finally 结构,异常继续向上抛,但 finally 保证资源关闭
|
|
|
|
|
+ Product product = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ product = findProduct(productId); // 可能抛出 ProductNotFoundException
|
|
|
|
|
+
|
|
|
|
|
+ // 检查库存是否充足
|
|
|
|
|
+ // 如果 product.getStock() < quantity,抛出 StockNotEnoughException
|
|
|
|
|
+ // 提示信息:"商品 " + product.getName() + " 库存不足!当前库存:"
|
|
|
|
|
+ // + product.getStock() + ",需要:" + quantity
|
|
|
|
|
+ // 请补充代码
|
|
|
|
|
+
|
|
|
|
|
+ // 减少库存
|
|
|
|
|
+ product.setStock(product.getStock() - quantity);
|
|
|
|
|
+ System.out.println("出库成功:" + product.getName() + " × " + quantity
|
|
|
|
|
+ + ",剩余库存:" + product.getStock());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ // 【新增】finally 块——模拟关闭数据库连接的操作
|
|
|
|
|
+ // 无论出库成功还是失败,都要关闭数据库连接
|
|
|
|
|
+ // 输出:"【数据库】已关闭商品库存连接(商品ID:" + productId + ")"
|
|
|
|
|
+ // 请补充代码
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 入库操作(增加库存)—— 编译期异常在方法内部 try-catch 处理
|
|
|
|
|
|
|
+ * 入库操作(增加库存)—— 编译期异常在方法内部 try-catch-finally 处理
|
|
|
* @param productId 商品编号
|
|
* @param productId 商品编号
|
|
|
* @param quantity 入库数量
|
|
* @param quantity 入库数量
|
|
|
*/
|
|
*/
|
|
@@ -1386,6 +1418,11 @@ public class InventoryManager {
|
|
|
+ ",当前库存:" + product.getStock());
|
|
+ ",当前库存:" + product.getStock());
|
|
|
} catch (ProductNotFoundException e) {
|
|
} catch (ProductNotFoundException e) {
|
|
|
System.out.println("入库失败:" + e.getMessage());
|
|
System.out.println("入库失败:" + e.getMessage());
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ // 【新增】finally 块——模拟关闭数据库连接
|
|
|
|
|
+ // 无论入库成功还是失败,都要关闭数据库连接
|
|
|
|
|
+ // 输出:"【数据库】已关闭商品库存连接(商品ID:" + productId + ")"
|
|
|
|
|
+ // 请补充代码
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1464,7 +1501,7 @@ public class InventoryTest {
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-### 预期输出
|
|
|
|
|
|
|
+### 预期输出(含 finally 数据库关闭)
|
|
|
|
|
|
|
|
```
|
|
```
|
|
|
========== 1. 添加商品 ==========
|
|
========== 1. 添加商品 ==========
|
|
@@ -1479,12 +1516,17 @@ Product{id='P003', name='小米平板', price=3499.0, stock=80}
|
|
|
|
|
|
|
|
========== 2. 出库操作 ==========
|
|
========== 2. 出库操作 ==========
|
|
|
出库成功:华为手机 × 10,剩余库存:90
|
|
出库成功:华为手机 × 10,剩余库存:90
|
|
|
|
|
+【数据库】已关闭商品库存连接(商品ID:P001)
|
|
|
出库失败:商品 小米平板 库存不足!当前库存:80,需要:200
|
|
出库失败:商品 小米平板 库存不足!当前库存:80,需要:200
|
|
|
|
|
+【数据库】已关闭商品库存连接(商品ID:P003)
|
|
|
出库失败:商品编号 P999 不存在!
|
|
出库失败:商品编号 P999 不存在!
|
|
|
|
|
+【数据库】已关闭商品库存连接(商品ID:P999)
|
|
|
|
|
|
|
|
========== 3. 入库操作 ==========
|
|
========== 3. 入库操作 ==========
|
|
|
入库成功:苹果笔记本 × 20,当前库存:70
|
|
入库成功:苹果笔记本 × 20,当前库存:70
|
|
|
|
|
+【数据库】已关闭商品库存连接(商品ID:P002)
|
|
|
入库失败:商品编号 P999 不存在!
|
|
入库失败:商品编号 P999 不存在!
|
|
|
|
|
+【数据库】已关闭商品库存连接(商品ID:P999)
|
|
|
|
|
|
|
|
===== 当前库存列表 =====
|
|
===== 当前库存列表 =====
|
|
|
Product{id='P001', name='华为手机', price=5999.0, stock=90}
|
|
Product{id='P001', name='华为手机', price=5999.0, stock=90}
|
|
@@ -1512,10 +1554,248 @@ Product{id='P003', name='小米平板', price=3499.0, stock=80}
|
|
|
3. 对比 `InventoryTest` 中调用 `outbound()`(需要 try-catch)和调用 `addProduct()`(不需要 try-catch),哪种异常更容易使用?哪种异常更容易被开发者忽略?
|
|
3. 对比 `InventoryTest` 中调用 `outbound()`(需要 try-catch)和调用 `addProduct()`(不需要 try-catch),哪种异常更容易使用?哪种异常更容易被开发者忽略?
|
|
|
4. 在实际项目开发中,什么情况下应该设计为编译期异常(Checked Exception),什么情况下应该设计为运行时异常(Unchecked Exception)?请结合本练习中的例子说明。
|
|
4. 在实际项目开发中,什么情况下应该设计为编译期异常(Checked Exception),什么情况下应该设计为运行时异常(Unchecked Exception)?请结合本练习中的例子说明。
|
|
|
5. 请阅读 `inbound()` 方法的代码:它在方法内部用 try-catch 处理了 `ProductNotFoundException`。这样做和 `outbound()` 方法把异常抛给调用者处理,各有什么优缺点?
|
|
5. 请阅读 `inbound()` 方法的代码:它在方法内部用 try-catch 处理了 `ProductNotFoundException`。这样做和 `outbound()` 方法把异常抛给调用者处理,各有什么优缺点?
|
|
|
|
|
+6. **【finally 思考】** `outbound()` 方法使用了 `try-finally` 结构(没有 catch 块)。这种写法有什么特点?相比于 `try-catch-finally`,它有什么不同的用途?(提示:异常会继续向上抛吗?)
|
|
|
|
|
+7. **【finally 思考】** 在 `outbound()` 方法的 `try-finally` 中,如果 `findProduct()` 抛出异常,`product.setStock()` 还会执行吗?`finally` 块还会执行吗?请描述完整的执行流程。
|
|
|
|
|
+8. **【finally 对比】** 本练习中 `outbound()` 使用 `try-finally`(不处理异常,只保证资源关闭),`inbound()` 使用 `try-catch-finally`(处理异常 + 保证资源关闭)。请分析两种结构分别在什么场景下使用。
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 📋 题目总览
|
|
|
|
|
|
|
+## 第 4 题:finally 专项练习——文件读取模拟器(try-finally + try-catch-finally + finally 执行特性)⭐⭐⭐⭐
|
|
|
|
|
+
|
|
|
|
|
+### 需求说明
|
|
|
|
|
+
|
|
|
|
|
+开发一个文件读取模拟器,核心目的是练习 `finally` 的各种使用场景和特性。不涉及真正的文件 IO,而是通过模拟来理解 `finally` 的执行时机、与 `return` 的配合、以及在各种异常情况下的行为。
|
|
|
|
|
+
|
|
|
|
|
+### 类设计
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+import java.util.Scanner;
|
|
|
|
|
+
|
|
|
|
|
+public class FileReadSimulator {
|
|
|
|
|
+
|
|
|
|
|
+ private static int fileOpenCount = 0; // 模拟文件打开计数器
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 任务 1:基本的 try-finally 结构
|
|
|
|
|
+ * 模拟打开文件 → 读取第一行 → 关闭文件
|
|
|
|
|
+ * 要求使用 try-finally 确保文件一定被关闭
|
|
|
|
|
+ */
|
|
|
|
|
+ public static void readFirstLine(String fileName) {
|
|
|
|
|
+ System.out.println("【打开文件】" + fileName);
|
|
|
|
|
+ fileOpenCount++;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ System.out.println("【读取】文件 " + fileName + " 的第一行内容:Hello, World!");
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟异常:如果文件名包含 "error",抛出 RuntimeException
|
|
|
|
|
+ if (fileName.contains("error")) {
|
|
|
|
|
+ throw new RuntimeException("文件 " + fileName + " 读取错误!");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("【读取完成】" + fileName);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ // 请补充代码:关闭文件,输出 "【关闭文件】" + fileName
|
|
|
|
|
+ // 注意:即使发生了异常,也要关闭文件
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 任务 2:finally + return 的执行顺序
|
|
|
|
|
+ * 验证 finally 块在 return 之前还是之后执行
|
|
|
|
|
+ */
|
|
|
|
|
+ public static int testReturnBeforeFinally() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ System.out.println("try 块中的代码");
|
|
|
|
|
+ return 1; // 有 return 语句
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ System.out.println("finally 块中的代码——它在 return 之前还是之后执行?");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 任务 3:finally 中修改返回值
|
|
|
|
|
+ * 验证 finally 块能否修改 try 块中的 return 值
|
|
|
|
|
+ */
|
|
|
|
|
+ public static int testFinallyModifyReturn() {
|
|
|
|
|
+ int result = 100;
|
|
|
|
|
+ try {
|
|
|
|
|
+ return result; // 返回 100?
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ result = 200; // finally 中修改了 result
|
|
|
|
|
+ System.out.println("finally 中 result = " + result);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 任务 4:try-finally 中异常传播
|
|
|
|
|
+ * finally 块中再次抛出异常会怎样?
|
|
|
|
|
+ */
|
|
|
|
|
+ public static void testExceptionInFinally() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ System.out.println("try 块:正常执行");
|
|
|
|
|
+ throw new RuntimeException("try 中的异常");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ System.out.println("finally 块:执行清理...");
|
|
|
|
|
+ // 如果在 finally 中也抛出异常,会发生什么?try 中的异常会被覆盖吗?
|
|
|
|
|
+ // 请取消注释下面的代码试运行
|
|
|
|
|
+ // throw new RuntimeException("finally 中的异常");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 任务 5:模拟带资源计数器的文件读取
|
|
|
|
|
+ * 使用 finally 确保每次打开文件后都正确关闭
|
|
|
|
|
+ */
|
|
|
|
|
+ public static void safeReadFile(String fileName) {
|
|
|
|
|
+ System.out.println("\n【开始处理】" + fileName);
|
|
|
|
|
+ // 要求:
|
|
|
|
|
+ // 1. 使用 try-catch-finally 结构
|
|
|
|
|
+ // 2. try 块中:打印 "【读取中】" + fileName,如果文件名包含 "error" 则抛异常
|
|
|
|
|
+ // 3. catch 块中:打印 "【捕获异常】" + e.getMessage()
|
|
|
|
|
+ // 4. finally 块中:打印 "【资源释放】" + fileName + " 已关闭"
|
|
|
|
|
+ // 同时将 fileOpenCount 减 1(表示关闭了一个文件)
|
|
|
|
|
+ // 请补充代码
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 任务 6:验证 finally 一定会执行(即使在 catch 中 return)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static boolean testFinallyAfterCatchReturn() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ System.out.println("try 块:执行中...");
|
|
|
|
|
+ int[] arr = {};
|
|
|
|
|
+ System.out.println(arr[0]); // 抛出 ArrayIndexOutOfBoundsException
|
|
|
|
|
+ } catch (ArrayIndexOutOfBoundsException e) {
|
|
|
|
|
+ System.out.println("catch 块:捕获到异常,准备 return false");
|
|
|
|
|
+ return false; // catch 中有 return
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ System.out.println("finally 块:catch 都 return 了,我还能执行吗?");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 不会执行到这里
|
|
|
|
|
+ // return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static void main(String[] args) {
|
|
|
|
|
+ System.out.println("========== 任务 1:基本的 try-finally ==========");
|
|
|
|
|
+ readFirstLine("config.txt");
|
|
|
|
|
+ readFirstLine("data_error.txt"); // 会抛出异常,但 finally 仍会关闭文件
|
|
|
|
|
+ System.out.println("任务 1 结束,程序继续运行"); // 注意:第二行抛异常后会终止吗?
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("\n========== 任务 2:finally 与 return 的顺序 ==========");
|
|
|
|
|
+ int ret1 = testReturnBeforeFinally();
|
|
|
|
|
+ System.out.println("返回值:" + ret1);
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("\n========== 任务 3:finally 修改返回值 ==========");
|
|
|
|
|
+ int ret2 = testFinallyModifyReturn();
|
|
|
|
|
+ System.out.println("返回值:" + ret2); // 输出 100 还是 200?
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("\n========== 任务 4:finally 中的异常 ==========");
|
|
|
|
|
+ try {
|
|
|
|
|
+ testExceptionInFinally();
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ System.out.println("main 中捕获:" + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("\n========== 任务 5:安全读取(try-catch-finally)==========");
|
|
|
|
|
+ safeReadFile("readme.txt");
|
|
|
|
|
+ safeReadFile("error.log");
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("\n========== 任务 6:finally 在 catch return 后 ==========");
|
|
|
|
|
+ boolean ret3 = testFinallyAfterCatchReturn();
|
|
|
|
|
+ System.out.println("返回值:" + ret3);
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("\n========== 文件打开计数器 ==========");
|
|
|
|
|
+ System.out.println("当前未关闭文件数:" + fileOpenCount); // 应该为 0(全部关闭)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 请回答以下问题(代码填空 + 分析)
|
|
|
|
|
+
|
|
|
|
|
+#### 问题 1:任务 2 的执行顺序
|
|
|
|
|
+
|
|
|
|
|
+运行任务 2,观察控制台输出的顺序:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+try 块中的代码
|
|
|
|
|
+finally 块中的代码——它在 return 之前还是之后执行?
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+请问:`finally` 块是在 `return` **之前**还是**之后**执行的?为什么返回值仍然是 1(而不是没有返回值)?
|
|
|
|
|
+
|
|
|
|
|
+#### 问题 2:任务 3 的返回值
|
|
|
|
|
+
|
|
|
|
|
+运行任务 3,观察输出:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+finally 中 result = 200
|
|
|
|
|
+返回值:100
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+为什么 finally 中已经将 `result` 修改为 200,但返回的仍然是 100?这说明 finally 中修改返回值的特点是什么?
|
|
|
|
|
+
|
|
|
|
|
+#### 问题 3:任务 4 的异常覆盖
|
|
|
|
|
+
|
|
|
|
|
+如果同时取消 `testExceptionInFinally()` 中两处 `throw` 的注释,运行后会输出什么?finally 中抛出的异常和 try 中抛出的异常,哪个会传播到调用者?
|
|
|
|
|
+
|
|
|
|
|
+#### 问题 4:任务 6 的 finally 执行
|
|
|
|
|
+
|
|
|
|
|
+运行任务 6,观察输出,回答问题:catch 块中已经 `return false` 了,`finally` 块还能执行吗?这说明 `finally` 的执行优先级有多高?
|
|
|
|
|
+
|
|
|
|
|
+#### 问题 5:请填写 finally 特性总结表
|
|
|
|
|
+
|
|
|
|
|
+| finally 特性 | 说明 |
|
|
|
|
|
+|-------------|------|
|
|
|
|
|
+| 是否一定会执行 | ①______ |
|
|
|
|
|
+| 与 return 的执行顺序 | ②______ |
|
|
|
|
|
+| 能否修改基本类型的返回值 | ③______ |
|
|
|
|
|
+| 能否修改引用类型的返回值(如对象属性) | ④______ |
|
|
|
|
|
+| finally 中抛出异常的影响 | ⑤______ |
|
|
|
|
|
+| 典型应用场景 | ⑥______ |
|
|
|
|
|
+
|
|
|
|
|
+### 预期输出(参考)
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+========== 任务 1:基本的 try-finally ==========
|
|
|
|
|
+【打开文件】config.txt
|
|
|
|
|
+【读取】文件 config.txt 的第一行内容:Hello, World!
|
|
|
|
|
+【读取完成】config.txt
|
|
|
|
|
+【关闭文件】config.txt
|
|
|
|
|
+【打开文件】data_error.txt
|
|
|
|
|
+【读取】文件 data_error.txt 的第一行内容:Hello, World!
|
|
|
|
|
+【关闭文件】data_error.txt
|
|
|
|
|
+Exception in thread "main" java.lang.RuntimeException: 文件 data_error.txt 读取错误! ← 异常继续向上抛
|
|
|
|
|
+
|
|
|
|
|
+注意:由于 readFirstLine 使用了 try-finally(没有 catch),
|
|
|
|
|
+异常会在 finally 执行完毕后继续向上传播。
|
|
|
|
|
+如果 main 中不捕获,JVM 会终止程序。
|
|
|
|
|
+因此任务 1 后面的任务可能不会执行。
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+> **思考**:如何修改 `readFirstLine` 的调用方式,让任务 1 中的异常不影响后续任务的执行?
|
|
|
|
|
+
|
|
|
|
|
+### 代码补充挑战
|
|
|
|
|
+
|
|
|
|
|
+请补全 `safeReadFile` 方法的完整实现:
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+public static void safeReadFile(String fileName) {
|
|
|
|
|
+ System.out.println("\n【开始处理】" + fileName);
|
|
|
|
|
+ // 请补充 try-catch-finally 实现
|
|
|
|
|
+ // try: 模拟读取文件,文件名含 "error" 则抛异常
|
|
|
|
|
+ // catch: 捕获异常并输出提示
|
|
|
|
|
+ // finally: 关闭文件,计数器减 1
|
|
|
|
|
+
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 思考题
|
|
|
|
|
+
|
|
|
|
|
+1. 任务 1 中 `readFirstLine` 使用了 `try-finally`(没有 catch),当发生异常时,`finally` 仍会执行,但异常会继续向上传播。这种「不处理异常、只保证清理」的模式在什么场景下非常有用?
|
|
|
|
|
+2. 任务 5 中 `safeReadFile` 使用了 `try-catch-finally`,和 `readFirstLine` 的 `try-finally` 相比,异常还会继续向上传播吗?为什么?
|
|
|
|
|
+3. 在实际开发中,`finally` 最常见的用途是关闭资源(如 `Scanner.close()`、文件流关闭、数据库连接关闭)。如果不使用 `finally`,把关闭代码放在 try 块末尾和每个 catch 块中各写一次,会有什么问题?
|
|
|
|
|
+4. 如果 try 块和 finally 块中都抛出了异常,最终调用者接收到的是哪个异常?try 块中的异常会丢失吗?(提示:这是 Java 异常抑制机制要解决的问题,JDK7+ 的 try-with-resources 提供了更好的方案。)
|
|
|
|
|
+5. 思考:在任务 1 中,如何让程序在发生异常时仍然能继续执行后续任务?结合前面学过的知识,给出至少两种方案。
|
|
|
|
|
|
|
|
| 序号 | 所属部分 | 核心知识点 | 难度 |
|
|
| 序号 | 所属部分 | 核心知识点 | 难度 |
|
|
|
|------|---------|-----------|------|
|
|
|------|---------|-----------|------|
|
|
@@ -1529,4 +1809,5 @@ Product{id='P003', name='小米平板', price=3499.0, stock=80}
|
|
|
| 练习 8 | 随堂 | throw 关键字手动抛出异常、业务逻辑校验、throw 执行特性 | ⭐⭐ |
|
|
| 练习 8 | 随堂 | throw 关键字手动抛出异常、业务逻辑校验、throw 执行特性 | ⭐⭐ |
|
|
|
| 进阶 1 | 进阶 | 自定义异常(继承 Exception)、业务校验、多 catch 块业务处理 | ⭐⭐⭐⭐ |
|
|
| 进阶 1 | 进阶 | 自定义异常(继承 Exception)、业务校验、多 catch 块业务处理 | ⭐⭐⭐⭐ |
|
|
|
| 进阶 2 | 进阶 | 自定义异常 + 业务逻辑 + 封装 + 异常处理最佳实践(银行 ATM) | ⭐⭐⭐⭐⭐ |
|
|
| 进阶 2 | 进阶 | 自定义异常 + 业务逻辑 + 封装 + 异常处理最佳实践(银行 ATM) | ⭐⭐⭐⭐⭐ |
|
|
|
-| 进阶 3 | 进阶 | 编译期异常 vs 运行时异常(继承 Exception vs RuntimeException)、throw + throws 完整实战、商品库存管理 | ⭐⭐⭐⭐ |
|
|
|
|
|
|
|
+| 进阶 3 | 进阶 | 编译期异常 vs 运行时异常(继承 Exception vs RuntimeException)、throw + throws + try-finally 完整实战、商品库存管理 | ⭐⭐⭐⭐ |
|
|
|
|
|
+| 进阶 4 | 进阶 | finally 专项练习:try-finally 结构、try-catch-finally 结构、finally 与 return 执行顺序、finally 修改返回值、finally 中的异常传播、资源释放计数器 | ⭐⭐⭐⭐ |
|