Browse Source

docs: 更新20260728异常处理练习题——新增异常常用方法练习(Exercise7)、throw手动抛出练习(Exercise8)、throw+throws+自定义异常编译期vs运行时综合实战(进阶3 商品库存管理系统); 更新题目总览表

WanJL 5 hours ago
parent
commit
edb6d0dbe8
1 changed files with 554 additions and 2 deletions
  1. 554 2
      课堂练习题/20260728-异常处理练习.md

+ 554 - 2
课堂练习题/20260728-异常处理练习.md

@@ -1,8 +1,8 @@
 # 2026-07-28 异常处理(Exception Handling)基础练习(课堂练习用)
 
-> **说明**:以下习题围绕今日授课核心内容——**Java 异常处理(Exception Handling)** 展开,涵盖 Throwable 体系结构、try-catch 多 catch 块捕获处理、常见运行时异常(数组下标越界、空指针、类型转换、算术异常等)、异常处理的鲁棒性等知识点。
+> **说明**:以下习题围绕今日授课核心内容——**Java 异常处理(Exception Handling)** 展开,涵盖 Throwable 体系结构、try-catch 多 catch 块捕获处理、异常常用方法(getMessage/printStackTrace/getCause)、常见运行时异常(数组下标越界、空指针、类型转换、算术异常等)、throw 与 throws 关键字、自定义异常、异常处理的鲁棒性等知识点。
 >
-> 题目分为两部分:**随堂练习**(异常处理基础,6 题)和 **进阶练习**(综合挑战,2 题)。
+> 题目分为两部分:**随堂练习**(异常处理基础,8 题)和 **进阶练习**(综合挑战,3 题)。
 
 ---
 
@@ -434,6 +434,246 @@ public static int divide(String s1, String s2) /* 请补充 throws 声明 */ {
 
 ---
 
+## 练习 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`:两者都能让方法提前结束,有什么区别?
+
+---
+
 # 第二部分:进阶练习
 
 > 以下为综合挑战题,融合多种异常类型、异常处理的最佳实践、自定义异常等知识点。
@@ -966,6 +1206,315 @@ public void withdraw(double amount)
 
 ---
 
+## 第 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 {
+        // 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-catch 处理
+     * @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());
+        }
+    }
+    
+    /**
+     * 显示所有商品信息
+     */
+    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();
+    }
+}
+```
+
+### 预期输出
+
+```
+========== 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
+出库失败:商品 小米平板 库存不足!当前库存:80,需要:200
+出库失败:商品编号 P999 不存在!
+
+========== 3. 入库操作 ==========
+入库成功:苹果笔记本 × 20,当前库存:70
+入库失败:商品编号 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()` 方法把异常抛给调用者处理,各有什么优缺点?
+
+---
+
 ## 📋 题目总览
 
 | 序号 | 所属部分 | 核心知识点 | 难度 |
@@ -976,5 +1525,8 @@ public void withdraw(double amount)
 | 练习 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 完整实战、商品库存管理 | ⭐⭐⭐⭐ |