# 2026-07-28 异常处理(Exception Handling)基础练习(课堂练习用) > **说明**:以下习题围绕今日授课核心内容——**Java 异常处理(Exception Handling)** 展开,涵盖 Throwable 体系结构、try-catch 多 catch 块捕获处理、异常常用方法(getMessage/printStackTrace/getCause)、常见运行时异常(数组下标越界、空指针、类型转换、算术异常等)、throw 与 throws 关键字、自定义异常、异常处理的鲁棒性等知识点。 > > 题目分为两部分:**随堂练习**(异常处理基础,8 题)和 **进阶练习**(综合挑战,4 题)。 --- # 第一部分:随堂练习 > 以下练习围绕异常处理的基础概念和用法,由浅入深。 --- ## 练习 1:数组下标越界异常捕获(ArrayIndexOutOfBoundsException) **难度**:⭐ **知识点**:try-catch 基本结构、数组下标越界异常捕获、多 catch 块、程序健壮性 **场景描述**:程序运行时经常会遇到数组下标越界的情况,如果不做处理,JVM 会终止程序运行。本练习要求使用 try-catch 捕获数组下标越界异常,让程序能够继续执行后续代码。 **题目**:完成以下 `Demo01` 类,创建一个长度为 3 的 int 数组,尝试访问索引 5 的元素,并用 try-catch 捕获异常。 ```java public class Demo01 { public static void main(String[] args) { int[] arr = {10, 20, 30}; // 长度 = 3,有效索引:0, 1, 2 // 请使用 try-catch 捕获数组下标越界异常 // 要求: // 1. 在 try 块中尝试访问 arr[5](会抛出 ArrayIndexOutOfBoundsException) // 2. 使用 catch 捕获 ArrayIndexOutOfBoundsException // 3. 输出:"数组下标越界!索引 5 超出了数组长度 " + arr.length // 4. 无论是否发生异常,最后都要输出:"程序继续运行..." // 请在此补充代码 } } ``` **预期输出**: ``` 数组下标越界!索引 5 超出了数组长度 3 程序继续运行... ``` **思考题**: 1. 如果 catch 块中写的是 `catch (Exception e)`,能否捕获到 `ArrayIndexOutOfBoundsException`?为什么? 2. 如果 try 块中先执行 `arr[5]`,再执行 `arr[1]`,`arr[1]` 还能正常执行吗?为什么? 3. try-catch 结构中的代码执行顺序是怎样的?(正常情况和异常情况分别说明) --- ## 练习 2:空指针异常捕获(NullPointerException) **难度**:⭐ **知识点**:空指针异常、引用类型默认值 null、数组元素为 null 的访问 **场景描述**:程序中经常会出现对 `null` 值调用方法或访问属性的情况,会触发 `NullPointerException`。本练习要求捕获空指针异常,使程序不会因一个空指针而完全崩溃。 **题目**:完成以下 `Demo02` 类,处理对象数组中的 null 元素。 ```java public class Demo02 { public static void main(String[] args) { String[] names = new String[5]; // 所有元素初始为 null names[0] = "张三"; names[2] = "李四"; names[4] = "王五"; // 要求:遍历 names 数组,对每个元素调用 .length() 方法获取字符串长度 // 如果元素为 null,会抛出 NullPointerException,请用 try-catch 捕获 // 输出格式:"元素 i 的长度为:xx"(正常) // 输出格式:"元素 i 为 null,跳过"(异常时) // 要求使用 try-catch 包裹在循环内部,让循环可以继续执行 for (int i = 0; i < names.length; i++) { // 请在此补充 try-catch 代码 } System.out.println("遍历完成!"); } } ``` **预期输出**: ``` 元素 0 的长度为:2 元素 1 为 null,跳过 元素 2 的长度为:2 元素 3 为 null,跳过 元素 4 的长度为:2 遍历完成! ``` **思考题**: 1. 如果把 try-catch 放在 for 循环**外面**,运行结果会有什么不同?为什么? 2. `NullPointerException` 是 `RuntimeException` 的子类吗?它属于编译期异常还是运行时异常? 3. 除了用 try-catch,还有什么方式可以避免空指针异常? --- ## 练习 3:类型转换异常捕获(ClassCastException) **难度**:⭐⭐ **知识点**:多态中的向下转型、ClassCastException、instanceof 运算符、类型安全的向下转型 **场景描述**:在多态编程中,向下转型(将父类引用强转为子类类型)时,如果对象的实际类型与目标类型不匹配,会抛出 `ClassCastException`。本练习通过 try-catch 捕获此类异常,同时练习使用 `instanceof` 来预防。 **题目**:定义动物类体系,完成以下 `Demo03` 类。 ```java // 已有类定义(无需修改) class Animal { public void speak() { System.out.println("动物发出叫声"); } } class Dog extends Animal { @Override public void speak() { System.out.println("汪汪汪!"); } public void guard() { System.out.println("狗狗在看家..."); } } class Cat extends Animal { @Override public void speak() { System.out.println("喵喵喵!"); } public void scratch() { System.out.println("猫咪在抓老鼠..."); } } ``` **要求**:完成 `Demo03` 的 `main` 方法。 ```java public class Demo03 { public static void main(String[] args) { Animal[] animals = new Animal[3]; animals[0] = new Dog(); animals[1] = new Cat(); animals[2] = new Dog(); // 任务 1:使用 try-catch 遍历数组,将每个元素向下转型为 Dog // 如果是 Dog 类型,调用 guard() 方法 // 如果是 Cat 类型,转型会抛出 ClassCastException,捕获并输出:"第 i 个元素不是 Dog,跳过" // 请补充代码 System.out.println("===== 任务 1:强制向下转型(可能抛异常)====="); for (int i = 0; i < animals.length; i++) { // 请在此补充 try-catch 代码 } // 任务 2:使用 instanceof 安全地进行向下转型(不依赖异常) System.out.println("\n===== 任务 2:使用 instanceof 安全转型 ====="); for (int i = 0; i < animals.length; i++) { // 请使用 instanceof 判断后再转型,调用 guard() 或 scratch() } } } ``` **预期输出**: ``` ===== 任务 1:强制向下转型(可能抛异常)===== 狗狗在看家... 第 1 个元素不是 Dog,跳过 狗狗在看家... ===== 任务 2:使用 instanceof 安全转型 ===== 狗狗在看家... 猫咪在抓老鼠... 狗狗在看家... ``` **思考题**: 1. 任务 1 和任务 2 两种方式各有什么优缺点?哪种方式更推荐? 2. `instanceof` 运算符的作用是什么?它能不能判断一个对象是否是某个接口的实现? 3. 使用异常来控制程序流程(如任务 1)有什么问题?(提示:异常处理的性能开销) --- ## 练习 4:算术异常与多 catch 块组合(ArithmeticException) **难度**:⭐⭐ **知识点**:ArithmeticException(除零异常)、多 catch 块的使用、catch 顺序(子类 → 父类) **场景描述**:在数学计算中,除零操作会引发 `ArithmeticException`。本练习要求使用多个 catch 块处理不同类型的异常,体会 catch 块的匹配顺序。 **题目**:完成以下 `Demo04` 类,模拟一个简易计算器的除法功能,处理多种输入异常。 ```java import java.util.Scanner; public class Demo04 { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); try { System.out.print("请输入被除数:"); String input1 = scanner.nextLine(); System.out.print("请输入除数:"); String input2 = scanner.nextLine(); int a = Integer.parseInt(input1); // 可能抛出 NumberFormatException int b = Integer.parseInt(input2); // 可能抛出 NumberFormatException int result = a / b; // 可能抛出 ArithmeticException System.out.println("计算结果:" + a + " ÷ " + b + " = " + result); } catch (NumberFormatException e) { System.out.println("输入格式错误:请输入整数数字!"); } catch (ArithmeticException e) { System.out.println("除数不能为零!"); } catch (RuntimeException e) { System.out.println("发生了运行时异常:" + e.getMessage()); } catch (Exception e) { System.out.println("发生了其他异常:" + e.getMessage()); } System.out.println("程序结束,感谢使用计算器!"); scanner.close(); } } ``` **测试运行要求**: 请根据以下输入情况,写出对应的输出结果: | 输入(被除数, 除数) | 预期输出 | |---------------------|---------| | 10, 2 | ①______ | | 10, 0 | ②______ | | abc, 5 | ③______ | | 10, null(直接回车) | ④______(提示:思考 `Integer.parseInt("")` 是否会抛异常) | **思考题**: 1. 本练习中 catch 块的顺序是 `NumberFormatException` → `ArithmeticException` → `RuntimeException` → `Exception`,如果调换顺序,比如把 `Exception` 放在最前面,会怎么样?(编译能通过吗?实际运行有什么问题?) 2. `NumberFormatException` 和 `ArithmeticException` 分别属于哪个异常类的子类? 3. 如果 try 块中第一行就抛出了异常,try 块中后面的代码还会执行吗? --- ## 练习 5:异常处理的鲁棒性——学生成绩处理系统 **难度**:⭐⭐⭐ **知识点**:try-catch-finally 综合运用、异常处理提升程序健壮性、多异常场景处理 **场景描述**:开发一个学生成绩处理系统,接收输入的学生成绩数组,计算总分和平均分。系统需要处理各种异常情况(空数组、null 元素、非法成绩值等),保证在任何异常情况下程序都能继续运行。 **题目**:完成以下 `ScoreManager` 类。 ```java public class ScoreManager { /** * 计算成绩数组的总分 * @param scores 成绩数组 * @return 总分,如果数组为 null 或长度为 0,返回 0 */ public static double sum(Double[] scores) { // 要求: // 1. 如果 scores 为 null,输出 "成绩数组为 null!",返回 0 // 2. 如果 scores 长度为 0,输出 "成绩数组为空!",返回 0 // 3. 遍历数组累加,如果某个元素为 null,输出 "第 i 个成绩为 null,跳过" // 但继续累加后续元素 // 4. 返回总分 // 请补充代码 return 0; } /** * 计算成绩数组的平均分 * @param scores 成绩数组 * @return 平均分,如果发生异常返回 0 */ public static double average(Double[] scores) { // 要求: // 1. 调用 sum() 方法获取总分 // 2. 计算有效成绩的个数(非 null 的元素个数) // 3. 如果有效个数为 0,输出 "没有有效成绩,无法计算平均分",返回 0 // 4. 返回总分 / 有效个数 // 5. 使用 try-catch 包裹整个方法逻辑,捕获任意异常,输出 "计算平均分时发生异常" // 并返回 0 // 请补充代码 return 0; } public static void main(String[] args) { // ===== 测试案例 1:正常数据 ===== Double[] scores1 = {85.5, 92.0, 78.5, 88.0, 95.5}; System.out.println("===== 学生成绩 1 ====="); System.out.println("总分:" + sum(scores1)); System.out.println("平均分:" + average(scores1)); System.out.println(); // ===== 测试案例 2:包含 null 元素 ===== Double[] scores2 = {85.5, null, 78.5, null, 95.5}; System.out.println("===== 学生成绩 2(含 null)====="); System.out.println("总分:" + sum(scores2)); System.out.println("平均分:" + average(scores2)); System.out.println(); // ===== 测试案例 3:空数组 ===== Double[] scores3 = {}; System.out.println("===== 学生成绩 3(空数组)====="); System.out.println("总分:" + sum(scores3)); System.out.println("平均分:" + average(scores3)); System.out.println(); // ===== 测试案例 4:null 数组 ===== Double[] scores4 = null; System.out.println("===== 学生成绩 4(null 数组)====="); System.out.println("总分:" + sum(scores4)); System.out.println("平均分:" + average(scores4)); } } ``` **预期输出**: ``` ===== 学生成绩 1 ===== 总分:439.5 平均分:87.9 ===== 学生成绩 2(含 null)===== 第 1 个成绩为 null,跳过 第 3 个成绩为 null,跳过 总分:259.5 平均分:86.5 ===== 学生成绩 3(空数组)===== 成绩数组为空! 总分:0.0 没有有效成绩,无法计算平均分 平均分:0.0 ===== 学生成绩 4(null 数组)===== 成绩数组为 null! 总分:0.0 计算平均分时发生异常 平均分:0.0 ``` **思考题**: 1. 为什么在 `average()` 方法中使用了 try-catch 包裹整个方法?如果不包裹,`scores = null` 的情况下会发生什么? 2. `sum()` 方法中先判断 null 再判断空数组,这个顺序能调换吗?为什么? 3. 这个学生成绩处理系统体现了什么编程思想?如果没有异常处理,用户在使用时会有怎样的体验? --- ## 练习 6:多 catch 块 + 异常向上抛出 **难度**:⭐⭐ **知识点**:多 catch 块精确匹配、异常类层次结构、catch 顺序规则、throws 抛出异常 **场景描述**:理解多 catch 块的匹配规则,以及何时使用 throws 将异常抛给调用者处理。 **题目**:阅读以下代码,回答问题。 ```java public class Demo06 { public static void methodA(int i) { try { if (i == 0) { int[] arr = {}; System.out.println(arr[0]); // 情况 A } else if (i == 1) { Object obj = null; obj.toString(); // 情况 B } else if (i == 2) { Object obj = new Integer(10); String str = (String) obj; // 情况 C } else if (i == 3) { int a = 10 / 0; // 情况 D } else if (i == 4) { int[] arr = new int[-1]; // 情况 E } } catch (ArrayIndexOutOfBoundsException e) { System.out.println("捕获:数组下标越界"); } catch (NullPointerException e) { System.out.println("捕获:空指针"); } catch (ClassCastException e) { System.out.println("捕获:类型转换"); } catch (ArithmeticException e) { System.out.println("捕获:算术异常"); } catch (RuntimeException e) { System.out.println("捕获:运行时异常:" + e.getClass().getSimpleName()); } System.out.println("方法执行完毕"); } public static void main(String[] args) { // 请写出每种情况的输出结果 // methodA(0); // 输出:①______ // methodA(1); // 输出:②______ // methodA(2); // 输出:③______ // methodA(3); // 输出:④______ // methodA(4); // 输出:⑤______ } } ``` **补充题**:请完成以下 `methodB` 方法,体会 throws 关键字的使用。 ```java // 请将以下方法补充完整: // 该方法接收两个字符串参数,将它们转换为整数并相除 // 方法内部不处理异常,将异常全部抛给调用者 public static int divide(String s1, String s2) /* 请补充 throws 声明 */ { // 请补充代码:将 s1 和 s2 转换为 int,返回 s1 / s2 // 不处理任何异常,直接抛出 } ``` **问题**: 1. 在 `methodA` 中,如果将最后一个 catch 块 `catch (RuntimeException e)` 移动到最前面,会有什么后果? 2. `NegativeArraySizeException` 是什么异常?它继承自哪个类? 3. `throws` 关键字和 `try-catch` 各有什么适用场景?什么情况下应该使用 `throws` 而不是 `try-catch`? 4. 如果 `methodB` 声明了 `throws NumberFormatException, ArithmeticException`,调用者调用 `methodB` 时是否必须使用 try-catch? --- ## 练习 7:异常常用方法(getMessage / printStackTrace / getCause) **难度**:⭐⭐ **知识点**:异常常用方法、`getMessage()`、`printStackTrace()`、`getCause()`、`fillInStackTrace()` **场景描述**:当 catch 捕获到异常对象后,可以调用 `Throwable` 类提供的各种方法来获取异常的详细信息。本练习要求在不同的异常场景中使用这些方法,理解它们各自的作用。 **题目**:完成以下 `ExceptionMethodDemo` 类。 ```java import java.io.FileNotFoundException; public class ExceptionMethodDemo { /** * 任务 1:体验 getMessage() 和 printStackTrace() * 传入一个数组和索引,尝试访问该索引的元素 */ public static void accessArray(int[] arr, int index) { try { System.out.println("数组元素:" + arr[index]); } catch (ArrayIndexOutOfBoundsException e) { // 1. 输出异常的描述信息(getMessage()) System.out.println("getMessage() 输出:" + e.getMessage()); // 2. 输出异常的完整堆栈信息(printStackTrace()) System.out.println("printStackTrace() 输出(请补充调用):"); // 请在下面调用 e.printStackTrace(); // 3. 输出异常的类名 System.out.println("异常类名:" + e.getClass().getName()); // 4. 输出异常的原因(getCause()) System.out.println("getCause() 输出:" + e.getCause()); } } /** * 任务 2:体验 fillInStackTrace() * 模拟多层方法调用中的异常传递 */ public static void methodA() throws Exception { methodB(); // 调用 methodB,methodB 会抛出异常 } public static void methodB() throws Exception { // 请在此处手动抛出一个 Exception,描述信息为 "methodB 中发生异常" // 使用 throw new Exception("...") } public static void testFillInStackTrace() { try { methodA(); } catch (Exception e) { System.out.println("原始异常信息:" + e.getMessage()); System.out.println("原始堆栈(调用 printStackTrace):"); e.printStackTrace(); // 显示原始异常发生位置 System.out.println("\n--- 以下为 fillInStackTrace 后的异常 ---"); // 调用 fillInStackTrace() 会重置堆栈信息到当前行 // 使得异常看起来像是在当前方法抛出的 Throwable newException = e.fillInStackTrace(); newException.printStackTrace(); } } /** * 任务 3:模拟异常链(getCause) * 将底层异常包装为高层异常,保留 cause 信息 */ public static void readFile(String path) throws Exception { try { // 模拟一个底层异常:文件未找到 throw new FileNotFoundException("文件 " + path + " 不存在!"); } catch (FileNotFoundException e) { // 将底层异常包装为高层异常,并保留 cause throw new Exception("读取文件失败,路径:" + path, e); } } public static void testExceptionChain() { try { readFile("/data/config.txt"); } catch (Exception e) { System.out.println("===== 异常链测试 ====="); System.out.println("高层异常信息:" + e.getMessage()); System.out.println("高层异常类名:" + e.getClass().getName()); // 获取底层异常(cause) Throwable cause = e.getCause(); System.out.println("\n底层异常(cause):" + cause); System.out.println("底层异常信息:" + cause.getMessage()); System.out.println("底层异常类名:" + cause.getClass().getName()); } } public static void main(String[] args) { System.out.println("========== 任务 1:getMessage 与 printStackTrace =========="); int[] arr = {1, 2, 3}; accessArray(arr, 5); System.out.println("\n========== 任务 2:fillInStackTrace =========="); testFillInStackTrace(); System.out.println("\n========== 任务 3:异常链 getCause =========="); testExceptionChain(); } } ``` ### 请回答以下问题 1. 运行任务 1,观察控制台输出,说说 `getMessage()` 返回的内容和 `printStackTrace()` 输出的内容有什么不同? 2. 任务 2 中,`fillInStackTrace()` 的作用是什么?调用前后 `printStackTrace` 输出的堆栈信息有什么变化? 3. 任务 3 中,`e.getCause()` 获取到的是什么?如果不使用带 `cause` 参数的构造方法(`new Exception(msg, cause)`),`getCause()` 会返回什么? 4. 请填写以下异常常用方法对比表: | 方法 | 返回值类型 | 作用 | |------|-----------|------| | `getMessage()` | ①______ | ②______ | | `printStackTrace()` | ③______ | ④______ | | `getCause()` | ⑤______ | ⑥______ | | `fillInStackTrace()` | ⑦______ | ⑧______ | --- ## 练习 8:throw 关键字——手动抛出异常 **难度**:⭐⭐ **知识点**:`throw` 关键字、手动创建异常对象、方法内抛出的业务逻辑判断 **场景描述**:在开发中,当方法检测到某种业务条件不满足时,可以使用 `throw` 关键字手动抛出异常。本练习要求在各种业务场景中,自主判断何时需要手动抛出异常。 **题目**:完成以下 `ThrowDemo` 类中的方法,使用 `throw` 手动抛出异常。 ```java public class ThrowDemo { /** * 任务 1:setAge() — 年龄校验 * 如果年龄 < 0 或 > 130,使用 throw 手动抛出 IllegalArgumentException * 提示信息:"年龄必须在 0~130 之间!" */ public static void setAge(int age) { if (age < 0 || age > 130) { // 请使用 throw 手动抛出 IllegalArgumentException // 请补充代码 } System.out.println("设置年龄成功:" + age); } /** * 任务 2:withdraw() — 银行取款校验 * 如果取款金额 <= 0,抛出 IllegalArgumentException * 提示信息:"取款金额必须大于 0!" * 如果取款金额 > 余额,抛出 IllegalArgumentException * 提示信息:"余额不足!当前余额:" + balance + " 元" * 如果通过校验,扣除余额并输出:"取款成功,剩余余额:" + balance + " 元" */ public static void withdraw(double balance, double amount) { // 请补充代码:校验金额合法性,手动抛出异常 // 请补充代码:校验余额是否充足,手动抛出异常 // 通过校验,执行取款 balance -= amount; System.out.println("取款成功,剩余余额:" + balance + " 元"); } /** * 任务 3:parseInt() — 字符串转整数(更友好的错误提示) * 如果传入的字符串为 null,抛出 NullPointerException * 提示信息:"输入的字符串不能为 null!" * 如果传入的字符串为空或只有空格,抛出 IllegalArgumentException * 提示信息:"输入的字符串不能为空!" * 如果字符串包含非数字字符,抛出 NumberFormatException * 提示信息:"字符串 \"" + str + "\" 包含非数字字符,无法转换!" * 通过校验后,返回 Integer.parseInt(str) */ public static int parseInt(String str) { // 请补充代码:判断 null // 请补充代码:判断空字符串 // 请补充代码:逐个字符检查是否都是数字(提示:str.toCharArray() 转字符数组遍历) // 如果不是数字,使用 throw 手动抛出 NumberFormatException // 通过校验,正常转换 return Integer.parseInt(str); } /** * 任务 4:throw 后代码不会执行 * 验证一旦执行了 throw 语句,后面的代码将不会被执行 */ public static void testThrowExit() { try { System.out.println("throw 之前的代码"); throw new RuntimeException("手动抛出的异常"); // System.out.println("throw 之后的代码"); // 编译错误:不可达语句 } catch (RuntimeException e) { System.out.println("捕获到异常:" + e.getMessage()); } System.out.println("catch 块之后的代码继续执行"); } public static void main(String[] args) { System.out.println("========== 任务 1:年龄校验 =========="); setAge(25); // 正常 setAge(-5); // 抛出异常 System.out.println("\n========== 任务 2:取款校验 =========="); withdraw(1000, 500); // 正常 withdraw(1000, -100); // 金额不合法 withdraw(1000, 2000); // 余额不足 System.out.println("\n========== 任务 3:字符串转整数 =========="); System.out.println(parseInt("123")); // 正常 → 123 // System.out.println(parseInt(null)); // 抛出 NullPointerException // System.out.println(parseInt("")); // 抛出 IllegalArgumentException // System.out.println(parseInt("12a3")); // 抛出 NumberFormatException System.out.println("\n========== 任务 4:throw 后的代码不会执行 =========="); testThrowExit(); } } ``` ### 思考题 1. 在任务 1 中,为什么 `throw` 后面不需要 `try-catch`?`IllegalArgumentException` 是编译期异常还是运行时异常? 2. 在任务 3 的 `parseInt()` 方法中,手动检查了字符串内容后调用了 `Integer.parseInt()`,这一步还有可能抛出异常吗?为什么? 3. 仔细看任务 4:注释说 `"throw 之后的代码"` 会编译报错(不可达语句),这说明了 `throw` 的执行特性是什么? 4. 对比 `throw` 和 `return`:两者都能让方法提前结束,有什么区别? --- # 第二部分:进阶练习 > 以下为综合挑战题,融合多种异常类型、异常处理的最佳实践、自定义异常等知识点。 --- ## 第 1 题:综合应用——用户注册验证系统(异常处理综合)⭐⭐⭐⭐ ### 需求说明 开发一个用户注册验证系统,对用户输入的注册信息进行校验。当输入不合法时,使用 **自定义异常** 和 **多 catch 处理** 给出明确提示。 ### 自定义异常类 定义以下自定义异常类(继承 `Exception` —— 编译期异常): ```java // 用户名不能为空异常 class UsernameEmptyException extends Exception { public UsernameEmptyException(String message) { super(message); } } // 用户名长度异常(长度必须在 3~12 个字符之间) class UsernameLengthException extends Exception { public UsernameLengthException(String message) { super(message); } } // 密码不能为空异常 class PasswordEmptyException extends Exception { public PasswordEmptyException(String message) { super(message); } } // 密码强度异常(必须包含数字和字母,长度 >= 6) class PasswordWeakException extends Exception { public PasswordWeakException(String message) { super(message); } } // 年龄异常(年龄必须在 0~150 之间) class AgeInvalidException extends Exception { public AgeInvalidException(String message) { super(message); } } ``` ### RegisterService 类 ```java public class RegisterService { /** * 验证用户名 * @param username 用户名 * @throws UsernameEmptyException 用户名为空 * @throws UsernameLengthException 用户名长度不在 3~12 之间 */ public static void validateUsername(String username) throws UsernameEmptyException, UsernameLengthException { // 1. 如果 username 为 null 或 trim() 后长度为 0,抛出 UsernameEmptyException // 提示信息:"用户名不能为空!" // 2. 如果 username.length() < 3 或 > 12,抛出 UsernameLengthException // 提示信息:"用户名长度必须在 3~12 个字符之间!" // 请补充代码 } /** * 验证密码 * @param password 密码 * @throws PasswordEmptyException 密码为空 * @throws PasswordWeakException 密码强度不足 */ public static void validatePassword(String password) throws PasswordEmptyException, PasswordWeakException { // 1. 如果 password 为 null 或 trim() 后长度为 0,抛出 PasswordEmptyException // 提示信息:"密码不能为空!" // 2. 如果 password.length() < 6,抛出 PasswordWeakException // 提示信息:"密码长度不能少于 6 位!" // 3. 如果 password 不包含数字或字母(判断技巧:使用正则或循环遍历逐个字符检查), // 抛出 PasswordWeakException // 提示信息:"密码必须包含数字和字母!" // 请补充代码(建议:使用循环遍历 + 两个 boolean 标记) } /** * 验证年龄 * @param age 年龄字符串 * @throws NumberFormatException 年龄不是数字 * @throws AgeInvalidException 年龄超出范围 */ public static void validateAge(String age) throws NumberFormatException, AgeInvalidException { // 1. 使用 Integer.parseInt(age) 转换为 int // 如果 age 不是有效数字,NumberFormatException 会自动抛出(无需手动处理) // 2. 如果 age < 0 或 age > 150,抛出 AgeInvalidException // 提示信息:"年龄必须在 0~150 之间!" // 请补充代码 } /** * 注册用户 * @param username 用户名 * @param password 密码 * @param age 年龄 */ public static void register(String username, String password, String age) { // 要求: // 1. 使用 try-catch-finally 调用各验证方法 // 2. 按顺序验证:用户名 → 密码 → 年龄 // 3. 每个验证都有独立的 catch 块,输出对应的错误提示 // 4. 如果所有验证都通过,输出:"注册成功!欢迎 " + username // 5. 注意:validateAge() 可能抛出 NumberFormatException,也需要捕获 // 6. 【新增】添加 finally 块,无论注册成功还是失败,都输出: // "—— 注册操作日志:用户名=" + username + ",结果=" + (是否成功 ? "成功" : "失败") // 提示:可以在 try 块成功时用一个 boolean 变量标记成功,finally 中读取该变量 // 请补充代码 } public static void main(String[] args) { // 测试用例 System.out.println("========== 测试 1:正常注册 =========="); register("张三丰", "abc123", "25"); System.out.println("\n========== 测试 2:用户名为空 =========="); register("", "abc123", "25"); System.out.println("\n========== 测试 3:用户名太短 =========="); register("12", "abc123", "25"); System.out.println("\n========== 测试 4:密码太短 =========="); register("张三丰", "12", "25"); System.out.println("\n========== 测试 5:密码不包含数字 =========="); register("张三丰", "abcdef", "25"); System.out.println("\n========== 测试 6:年龄不是数字 =========="); register("张三丰", "abc123", "二十五"); System.out.println("\n========== 测试 7:年龄超出范围 =========="); register("张三丰", "abc123", "200"); System.out.println("\n========== 测试 8:综合错误(多错误按顺序只报第一个)=========="); register("", "", "abc"); // 应该只报用户名错误,不会检查密码和年龄 } } ``` ### 预期输出(含 finally 操作日志) ``` ========== 测试 1:正常注册 ========== 注册成功!欢迎 张三丰 —— 注册操作日志:用户名=张三丰,结果=成功 ========== 测试 2:用户名为空 ========== 用户名不能为空! —— 注册操作日志:用户名=,结果=失败 ========== 测试 3:用户名太短 ========== 用户名长度必须在 3~12 个字符之间! —— 注册操作日志:用户名=12,结果=失败 ========== 测试 4:密码太短 ========== 密码长度不能少于 6 位! —— 注册操作日志:用户名=张三丰,结果=失败 ========== 测试 5:密码不包含数字 ========== 密码必须包含数字和字母! —— 注册操作日志:用户名=张三丰,结果=失败 ========== 测试 6:年龄不是数字 ========== 年龄格式错误:二十五 不是有效的数字! —— 注册操作日志:用户名=张三丰,结果=失败 ========== 测试 7:年龄超出范围 ========== 年龄必须在 0~150 之间! —— 注册操作日志:用户名=张三丰,结果=失败 ========== 测试 8:综合错误 ========== 用户名不能为空! —— 注册操作日志:用户名=,结果=失败 ``` ### 思考题 1. 本练习中,自定义异常都继承了 `Exception`(编译期异常),这有什么影响?如果改为继承 `RuntimeException`,代码会有什么变化? 2. 为什么 `register()` 方法中,每个验证方法都有自己的 catch 块,而不是用一个大 `catch (Exception e)` 统一处理?这样做的好处是什么? 3. 测试 8 中,用户名、密码、年龄同时出错,为什么只报了用户名的错误?这体现了什么设计思想? 4. 自定义异常在实际项目中有哪些应用场景?什么时候应该自定义异常而不是使用 Java 内置异常? 5. **【finally 思考】** 本练习中 `register()` 方法使用 `finally` 输出了操作日志。如果不用 `finally`,把日志输出放在 try 块末尾和每个 catch 块中分别写一次,是否能达到同样效果?这样做有什么弊端?(提示:代码重复、遗漏风险) 6. **【finally 思考】** 如果 try 块中有 `return` 语句,`finally` 块还会执行吗?请编写一个小测试验证你的结论。 --- ## 第 2 题:综合应用——银行 ATM 取款系统(异常处理 + 封装 + 业务逻辑)⭐⭐⭐⭐⭐ ### 需求说明 模拟银行 ATM 取款系统。该系统包含账户类、ATM 机类和自定义业务异常类。要求使用异常处理来处理各种业务情况(余额不足、取款金额超限、账户不存在等),体现 **异常处理在业务逻辑中的价值**。 ### 类设计 #### 自定义异常类(继承 Exception —— 编译期异常) ```java // 账户不存在异常 class AccountNotFoundException extends Exception { public AccountNotFoundException(String message) { super(message); } } // 余额不足异常 class InsufficientBalanceException extends Exception { public InsufficientBalanceException(String message) { super(message); } } // 取款金额超限异常(单次取款不能超过 5000 元) class WithdrawLimitExceededException extends Exception { public WithdrawLimitExceededException(String message) { super(message); } } // 无效金额异常(取款金额必须是正数,且为 100 的倍数) class InvalidAmountException extends Exception { public InvalidAmountException(String message) { super(message); } } ``` #### Account 类 ```java public class Account { private String accountId; // 账号 private String holderName; // 户主姓名 private double balance; // 余额 // 构造方法 public Account(String accountId, String holderName, double initialBalance) { this.accountId = accountId; this.holderName = holderName; this.balance = initialBalance; } // getter 方法 public String getAccountId() { return accountId; } public String getHolderName() { return holderName; } public double getBalance() { return balance; } /** * 存款 * @param amount 存款金额 */ public void deposit(double amount) { if (amount <= 0) { System.out.println("存款金额必须大于 0!"); return; } balance += amount; System.out.println(holderName + " 存入 " + amount + " 元,余额:" + balance + " 元"); } /** * 取款 * @param amount 取款金额 * @throws InvalidAmountException 金额不合法 * @throws WithdrawLimitExceededException 超过单次取款限制 * @throws InsufficientBalanceException 余额不足 */ public void withdraw(double amount) throws InvalidAmountException, WithdrawLimitExceededException, InsufficientBalanceException { // 1. 如果 amount <= 0 或者 amount % 100 != 0,抛出 InvalidAmountException // 提示信息:"取款金额必须为正数且为 100 的倍数!" // 2. 如果 amount > 5000,抛出 WithdrawLimitExceededException // 提示信息:"单次取款金额不能超过 5000 元!" // 3. 如果 amount > balance,抛出 InsufficientBalanceException // 提示信息:"余额不足!当前余额:" + balance + " 元,需要:" + amount + " 元" // 4. 通过所有检查后,扣除余额,输出: // holderName + " 取出 " + amount + " 元,剩余余额:" + balance + " 元" // 请补充代码 } /** * 查询余额 */ public void showBalance() { System.out.println("===== " + holderName + " 的账户 ====="); System.out.println("账号:" + accountId); System.out.println("余额:" + balance + " 元"); } } ``` #### ATM 类 ```java public class ATM { private String location; // ATM 位置 private Account[] accounts; // 管理的账户数组 private int accountCount; // 实际账户数量 public ATM(String location, int maxAccounts) { this.location = location; this.accounts = new Account[maxAccounts]; this.accountCount = 0; } /** * 添加账户到 ATM 系统 */ public void addAccount(Account account) { if (accountCount < accounts.length) { accounts[accountCount] = account; accountCount++; System.out.println("账户 " + account.getAccountId() + " 已添加到 " + location + " ATM 系统"); } else { System.out.println("ATM 账户已满,无法添加新账户!"); } } /** * 根据账号查找账户 * @param accountId 账号 * @return 账户对象 * @throws AccountNotFoundException 账户不存在 */ private Account findAccount(String accountId) throws AccountNotFoundException { // 遍历 accounts 数组查找匹配的账号 // 如果找到,返回该 Account 对象 // 如果没找到,抛出 AccountNotFoundException // 提示信息:"账号 " + accountId + " 不存在!" // 请补充代码 return null; } /** * ATM 取款 * @param accountId 账号 * @param amount 取款金额 */ public void withdraw(String accountId, double amount) { System.out.println("\n【" + location + " ATM】取款操作:账号 " + accountId + ",金额 " + amount + " 元"); // 要求: // 1. 使用 try-catch-finally 调用 findAccount() 查找账户 // 2. 找到账户后,调用 account.withdraw(amount) 执行取款 // 3. 捕获以下异常并给出提示: // - AccountNotFoundException:输出 "取款失败:账号不存在!" // - InvalidAmountException:输出 "取款失败:金额不合法!" + e.getMessage() // - WithdrawLimitExceededException:输出 "取款失败:超过限额!" + e.getMessage() // - InsufficientBalanceException:输出 "取款失败:余额不足!" + e.getMessage() // 4. 【新增】在 finally 块中输出交易记录: // "【交易记录】" + location + " | 取款 | 账号 " + accountId + " | 金额 " + amount + " 元" // 5. finally 块之后输出:"感谢使用 " + location + " ATM 服务!" // 请补充代码 } /** * ATM 存款 * @param accountId 账号 * @param amount 存款金额 */ public void deposit(String accountId, double amount) { System.out.println("\n【" + location + " ATM】存款操作:账号 " + accountId + ",金额 " + amount + " 元"); // 要求: // 1. 使用 try-catch-finally 调用 findAccount() 查找账户 // 2. 找到账户后,调用 account.deposit(amount) 执行存款 // 3. 捕获 AccountNotFoundException,输出:"存款失败:" + e.getMessage() // 4. 【新增】在 finally 块中输出交易记录: // "【交易记录】" + location + " | 存款 | 账号 " + accountId + " | 金额 " + amount + " 元" // 5. finally 块之后输出:"感谢使用 " + location + " ATM 服务!" // 请补充代码 } /** * 查询余额 */ public void checkBalance(String accountId) { System.out.println("\n【" + location + " ATM】余额查询:账号 " + accountId); try { Account account = findAccount(accountId); account.showBalance(); } catch (AccountNotFoundException e) { System.out.println("查询失败:" + e.getMessage()); } } } ``` ### 测试类要求 ```java public class ATMTest { public static void main(String[] args) { // 1. 创建 ATM 对象(位置:"幸福路支行",最大账户数:10) ATM atm = new ATM("幸福路支行", 10); // 2. 创建几个账户 Account acc1 = new Account("A001", "张三", 10000); Account acc2 = new Account("A002", "李四", 5000); Account acc3 = new Account("A003", "王五", 2000); // 3. 将账户添加到 ATM atm.addAccount(acc1); atm.addAccount(acc2); atm.addAccount(acc3); // 4. 测试正常取款 System.out.println("\n========== 正常取款 =========="); atm.withdraw("A001", 2000); // 成功取出 2000 // 5. 测试余额不足 System.out.println("\n========== 余额不足 =========="); atm.withdraw("A003", 3000); // 王五只有 2000,不足 // 6. 测试超过单次取款限额 System.out.println("\n========== 超过限额 =========="); atm.withdraw("A001", 6000); // 超过 5000 限额 // 7. 测试金额不合法 System.out.println("\n========== 金额不合法 =========="); atm.withdraw("A002", 123); // 不是 100 的倍数 // 8. 测试账户不存在 System.out.println("\n========== 账户不存在 =========="); atm.withdraw("A999", 1000); // 不存在的账户 // 9. 测试存款 System.out.println("\n========== 存款 =========="); atm.deposit("A002", 3000); // 存入 3000 // 10. 测试查询余额 System.out.println("\n========== 查询余额 =========="); atm.checkBalance("A001"); atm.checkBalance("A002"); atm.checkBalance("A003"); // 11. 测试查询不存在的账户 System.out.println("\n========== 查询不存在的账户 =========="); atm.checkBalance("A999"); } } ``` ### 预期输出(含 finally 交易记录) ``` 账户 A001 已添加到 幸福路支行 ATM 系统 账户 A002 已添加到 幸福路支行 ATM 系统 账户 A003 已添加到 幸福路支行 ATM 系统 ========== 正常取款 ========== 【幸福路支行 ATM】取款操作:账号 A001,金额 2000.0 元 张三 取出 2000.0 元,剩余余额:8000.0 元 【交易记录】幸福路支行 | 取款 | 账号 A001 | 金额 2000.0 元 感谢使用 幸福路支行 ATM 服务! ========== 余额不足 ========== 【幸福路支行 ATM】取款操作:账号 A003,金额 3000.0 元 取款失败:余额不足!当前余额:2000.0 元,需要:3000.0 元 【交易记录】幸福路支行 | 取款 | 账号 A003 | 金额 3000.0 元 感谢使用 幸福路支行 ATM 服务! ========== 超过限额 ========== 【幸福路支行 ATM】取款操作:账号 A001,金额 6000.0 元 取款失败:超过限额!单次取款金额不能超过 5000 元! 【交易记录】幸福路支行 | 取款 | 账号 A001 | 金额 6000.0 元 感谢使用 幸福路支行 ATM 服务! ========== 金额不合法 ========== 【幸福路支行 ATM】取款操作:账号 A002,金额 123.0 元 取款失败:金额不合法!取款金额必须为正数且为 100 的倍数! 【交易记录】幸福路支行 | 取款 | 账号 A002 | 金额 123.0 元 感谢使用 幸福路支行 ATM 服务! ========== 账户不存在 ========== 【幸福路支行 ATM】取款操作:账号 A999,金额 1000.0 元 取款失败:账号不存在! 【交易记录】幸福路支行 | 取款 | 账号 A999 | 金额 1000.0 元 感谢使用 幸福路支行 ATM 服务! ========== 存款 ========== 【幸福路支行 ATM】存款操作:账号 A002,金额 3000.0 元 李四 存入 3000.0 元,余额:8000.0 元 【交易记录】幸福路支行 | 存款 | 账号 A002 | 金额 3000.0 元 感谢使用 幸福路支行 ATM 服务! ========== 查询余额 ========== 【幸福路支行 ATM】余额查询:账号 A001 ===== 张三 的账户 ===== 账号:A001 余额:8000.0 元 【幸福路支行 ATM】余额查询:账号 A002 ===== 李四 的账户 ===== 账号:A002 余额:8000.0 元 【幸福路支行 ATM】余额查询:账号 A003 ===== 王五 的账户 ===== 账号:A003 余额:2000.0 元 ========== 查询不存在的账户 ========== 【幸福路支行 ATM】余额查询:账号 A999 查询失败:账号 A999 不存在! ``` ### 代码补充挑战 请补全 `Account.withdraw()` 方法中的异常抛出逻辑(参考前面给出的方法签名和注释): ```java public void withdraw(double amount) throws InvalidAmountException, WithdrawLimitExceededException, InsufficientBalanceException { // 请补充完整代码 } ``` ### 思考题 1. 本练习中,`Account.withdraw()` 将异常抛给了调用者(`ATM.withdraw()`),而不是在自己内部处理。这种设计有什么好处? 2. `ATM.withdraw()` 使用了多个 catch 块分别处理不同的业务异常,这和用一个 `catch (Exception e)` 统一处理相比,哪种方式更好?为什么? 3. 本练习中的业务异常(如 `InsufficientBalanceException`)继承了 `Exception`(编译期异常),如果改为继承 `RuntimeException`,`ATM.withdraw()` 中的 catch 能不能省略?省略后会有什么问题? 4. 在实际的银行系统中,除了本练习提到的异常,还可能有哪些业务异常?请列举至少 3 个。 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 中的输出语句的执行顺序,说明异常发生时这三部分的执行流程。 --- ## 第 3 题:综合应用——商品库存管理系统(throw + throws + 自定义异常 + 编译期 vs 运行时异常对比)⭐⭐⭐⭐ ### 需求说明 开发一个商品库存管理系统,核心要求: 1. 定义两个自定义异常——继承 `Exception`(编译期)和继承 `RuntimeException`(运行时) 2. 使用 `throw` 手动抛出异常 3. 使用 `throws` 声明异常 4. 体会编译期异常和运行时异常在强制处理上的区别 ### 需求分解 #### 自定义异常类 ```java // ===== 编译期异常(继承 Exception)===== // 商品不存在异常 class ProductNotFoundException extends Exception { public ProductNotFoundException(String message) { super(message); } } // 库存不足异常 class StockNotEnoughException extends Exception { public StockNotEnoughException(String message) { super(message); } } // ===== 运行时异常(继承 RuntimeException)===== // 商品价格异常(价格为负数或为 0) class InvalidPriceException extends RuntimeException { public InvalidPriceException(String message) { super(message); } } // 商品数量异常(数量为负数) class InvalidQuantityException extends RuntimeException { public InvalidQuantityException(String message) { super(message); } } ``` #### Product 类 ```java public class Product { private String id; // 商品编号 private String name; // 商品名称 private double price; // 单价 private int stock; // 库存数量 // 构造方法 public Product(String id, String name, double price, int stock) { this.id = id; this.name = name; // 调用 setPrice 和 setStock 进行校验 setPrice(price); setStock(stock); } // getter 方法 public String getId() { return id; } public String getName() { return name; } public double getPrice() { return price; } public int getStock() { return stock; } /** * 设置价格(运行时异常) * 如果 price <= 0,抛出 InvalidPriceException * 提示信息:"商品价格必须大于 0!当前价格:" + price */ public void setPrice(double price) { // 请补充代码:使用 throw 抛出 InvalidPriceException(运行时异常) // 注意:因为 InvalidPriceException 继承 RuntimeException, // 所以方法上不需要 throws 声明 this.price = price; } /** * 设置库存数量(运行时异常) * 如果 stock < 0,抛出 InvalidQuantityException * 提示信息:"商品库存数量不能为负数!当前数量:" + stock */ public void setStock(int stock) { // 请补充代码:使用 throw 抛出 InvalidQuantityException(运行时异常) this.stock = stock; } @Override public String toString() { return "Product{id='" + id + "', name='" + name + "', price=" + price + ", stock=" + stock + "}"; } } ``` #### InventoryManager 类 ```java public class InventoryManager { private Product[] products; // 商品数组 private int count; // 实际商品数量 public InventoryManager(int capacity) { this.products = new Product[capacity]; this.count = 0; } /** * 添加商品(运行时异常自行处理) * 如果商品数组已满,抛出 RuntimeException * 提示信息:"库存管理系统已满,无法添加新商品!" */ public void addProduct(Product product) { if (count >= products.length) { // 请补充代码:使用 throw 抛出 RuntimeException(运行时异常) } products[count] = product; count++; System.out.println("商品已入库:" + product.getName()); } /** * 根据商品编号查找商品 * @param productId 商品编号 * @return 商品对象 * @throws ProductNotFoundException 商品不存在(编译期异常) */ public Product findProduct(String productId) throws ProductNotFoundException { for (int i = 0; i < count; i++) { if (products[i].getId().equals(productId)) { return products[i]; } } // 请补充代码:使用 throw 抛出 ProductNotFoundException(编译期异常) // 提示信息:"商品编号 " + productId + " 不存在!" return null; } /** * 出库操作(减少库存) * @param productId 商品编号 * @param quantity 出库数量 * @throws ProductNotFoundException 商品不存在 * @throws StockNotEnoughException 库存不足 */ public void outbound(String productId, int quantity) throws ProductNotFoundException, StockNotEnoughException { // 注意:此方法使用 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-finally 处理 * @param productId 商品编号 * @param quantity 入库数量 */ public void inbound(String productId, int quantity) { try { // 调用 findProduct(),这里需要处理 ProductNotFoundException Product product = findProduct(productId); product.setStock(product.getStock() + quantity); System.out.println("入库成功:" + product.getName() + " × " + quantity + ",当前库存:" + product.getStock()); } catch (ProductNotFoundException e) { System.out.println("入库失败:" + e.getMessage()); } finally { // 【新增】finally 块——模拟关闭数据库连接 // 无论入库成功还是失败,都要关闭数据库连接 // 输出:"【数据库】已关闭商品库存连接(商品ID:" + productId + ")" // 请补充代码 } } /** * 显示所有商品信息 */ public void showAllProducts() { System.out.println("\n===== 当前库存列表 ====="); if (count == 0) { System.out.println("仓库为空!"); return; } for (int i = 0; i < count; i++) { System.out.println(products[i]); } } } ``` ### 测试类要求 ```java public class InventoryTest { public static void main(String[] args) { InventoryManager manager = new InventoryManager(5); // ===== 1. 添加商品 ===== System.out.println("========== 1. 添加商品 =========="); // 正常添加 manager.addProduct(new Product("P001", "华为手机", 5999.0, 100)); manager.addProduct(new Product("P002", "苹果笔记本", 12999.0, 50)); manager.addProduct(new Product("P003", "小米平板", 3499.0, 80)); // 以下两行会抛出运行时异常,应如何处理? // 思考:因为是 RuntimeException,不强制 try-catch // 尝试取消注释,观察运行效果 // manager.addProduct(new Product("P004", "测试手机", -100, 10)); // 价格异常 // manager.addProduct(new Product("P005", "测试平板", 2000, -5)); // 数量异常 manager.showAllProducts(); // ===== 2. 出库操作(编译期异常,必须 try-catch 或 throws)===== System.out.println("\n========== 2. 出库操作 =========="); // 正常出库 try { manager.outbound("P001", 10); } catch (ProductNotFoundException e) { System.out.println("出库失败:" + e.getMessage()); } catch (StockNotEnoughException e) { System.out.println("出库失败:" + e.getMessage()); } // 库存不足 try { manager.outbound("P003", 200); // 库存只有 80 } catch (ProductNotFoundException | StockNotEnoughException e) { System.out.println("出库失败:" + e.getMessage()); } // 商品不存在 try { manager.outbound("P999", 10); } catch (ProductNotFoundException | StockNotEnoughException e) { System.out.println("出库失败:" + e.getMessage()); } // ===== 3. 入库操作(编译期异常已在 inbound 内部处理)===== System.out.println("\n========== 3. 入库操作 =========="); manager.inbound("P002", 20); // 正常入库 manager.inbound("P999", 10); // 商品不存在,内部已捕获 // ===== 4. 显示最终库存 ===== manager.showAllProducts(); } } ``` ### 预期输出(含 finally 数据库关闭) ``` ========== 1. 添加商品 ========== 商品已入库:华为手机 商品已入库:苹果笔记本 商品已入库:小米平板 ===== 当前库存列表 ===== Product{id='P001', name='华为手机', price=5999.0, stock=100} Product{id='P002', name='苹果笔记本', price=12999.0, stock=50} Product{id='P003', name='小米平板', price=3499.0, stock=80} ========== 2. 出库操作 ========== 出库成功:华为手机 × 10,剩余库存:90 【数据库】已关闭商品库存连接(商品ID:P001) 出库失败:商品 小米平板 库存不足!当前库存:80,需要:200 【数据库】已关闭商品库存连接(商品ID:P003) 出库失败:商品编号 P999 不存在! 【数据库】已关闭商品库存连接(商品ID:P999) ========== 3. 入库操作 ========== 入库成功:苹果笔记本 × 20,当前库存:70 【数据库】已关闭商品库存连接(商品ID:P002) 入库失败:商品编号 P999 不存在! 【数据库】已关闭商品库存连接(商品ID:P999) ===== 当前库存列表 ===== Product{id='P001', name='华为手机', price=5999.0, stock=90} Product{id='P002', name='苹果笔记本', price=12999.0, stock=70} Product{id='P003', name='小米平板', price=3499.0, stock=80} ``` ### 对比分析填空 请根据本练习的代码,完成下面的对比表格: | 对比维度 | 编译期异常(继承 Exception) | 运行时异常(继承 RuntimeException) | |---------|---------------------------|----------------------------------| | 异常类示例 | ①______、②______ | ③______、④______ | | 是否需要 throws 声明 | ⑤______ | ⑥______ | | 调用者是否必须处理(try-catch) | ⑦______ | ⑧______ | | 典型场景 | ⑨______ | ⑩______ | | 方法 `setPrice()` 抛出的异常类型 | ⑪______ | —— | | 方法 `findProduct()` 抛出的异常类型 | —— | ⑫______ | ### 思考题 1. 在本练习中,`setPrice()` 抛出的是运行时异常,所以方法上不需要 `throws InvalidPriceException` 声明。如果将 `InvalidPriceException` 改为继承 `Exception`,`setPrice()` 方法需要做什么修改?哪些调用 `setPrice()` 的地方需要修改? 2. `outbound()` 方法声明了 `throws ProductNotFoundException, StockNotEnoughException`,在 `InventoryTest` 的 main 方法中调用 `outbound()` 时,编译期是否会报错?main 方法是否也必须用 `throws` 抛出? 3. 对比 `InventoryTest` 中调用 `outbound()`(需要 try-catch)和调用 `addProduct()`(不需要 try-catch),哪种异常更容易使用?哪种异常更容易被开发者忽略? 4. 在实际项目开发中,什么情况下应该设计为编译期异常(Checked Exception),什么情况下应该设计为运行时异常(Unchecked Exception)?请结合本练习中的例子说明。 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 中,如何让程序在发生异常时仍然能继续执行后续任务?结合前面学过的知识,给出至少两种方案。 | 序号 | 所属部分 | 核心知识点 | 难度 | |------|---------|-----------|------| | 练习 1 | 随堂 | ArrayIndexOutOfBoundsException 捕获、try-catch 基本结构 | ⭐ | | 练习 2 | 随堂 | NullPointerException 捕获、异常在循环内外的不同影响 | ⭐ | | 练习 3 | 随堂 | ClassCastException 捕获、instanceof 安全转型对比 | ⭐⭐ | | 练习 4 | 随堂 | ArithmeticException、多 catch 块顺序(子类→父类) | ⭐⭐ | | 练习 5 | 随堂 | 异常处理与程序鲁棒性、多异常场景处理、try-catch 综合运用 | ⭐⭐⭐ | | 练习 6 | 随堂 | 多 catch 块精确匹配、异常类层次结构、throws 抛出 | ⭐⭐ | | 练习 7 | 随堂 | 异常常用方法(getMessage / printStackTrace / getCause / fillInStackTrace) | ⭐⭐ | | 练习 8 | 随堂 | throw 关键字手动抛出异常、业务逻辑校验、throw 执行特性 | ⭐⭐ | | 进阶 1 | 进阶 | 自定义异常(继承 Exception)、业务校验、多 catch 块业务处理 | ⭐⭐⭐⭐ | | 进阶 2 | 进阶 | 自定义异常 + 业务逻辑 + 封装 + 异常处理最佳实践(银行 ATM) | ⭐⭐⭐⭐⭐ | | 进阶 3 | 进阶 | 编译期异常 vs 运行时异常(继承 Exception vs RuntimeException)、throw + throws + try-finally 完整实战、商品库存管理 | ⭐⭐⭐⭐ | | 进阶 4 | 进阶 | finally 专项练习:try-finally 结构、try-catch-finally 结构、finally 与 return 执行顺序、finally 修改返回值、finally 中的异常传播、资源释放计数器 | ⭐⭐⭐⭐ |