本课作品
制作安全背包管理员 SafeInventory
用户输入“很多”,程序不必立刻结束。你会读懂异常线索,捕获能够恢复的失败,在写入库存前检查全部条件,并给出可行动的提示。 前置背包:方法、条件判断、Map 和字符串转换。今天玩家输入很多颗钻石时,程序不会像苦力怕一样原地爆炸。
开始前:建议先完成第 25–27 课
- 异常类型说明哪类操作失败
- 消息给出具体输入线索
- 源文件行号定位自己的代码
01 / 15
开始前:先预测,再让程序回答
本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。
每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。
终端命令:中文占位符换成实际文件名与类名
javac -encoding UTF-8 示例文件名.java
java 示例类名- 预测:先写预期输出,或画清引用和数据的变化。
- 验证:运行后逐行核对,再只改一个条件。
- 解释:说清结果来自 Java 规则、课程合同还是输入数据。
02 / 15
异常报告失败的操作
异常不是程序员被扣分的通知,而是运行时发现操作无法正常完成后送出的故障报告。
堆栈跟踪会列出方法调用路径。先看异常类型和消息,再找第一条指向自己源文件的行。
- 异常类型说明哪类操作失败
- 消息给出具体输入线索
- 源文件行号定位自己的代码
BadNumber.java · 故意错误示例
public class BadNumber {
public static void main(String[] args) {
int amount = Integer.parseInt("很多");
System.out.println(amount);
}
}03 / 15
try catch 处理可恢复错误
字符串转整数可能失败。若用户可以重新输入,就捕获 NumberFormatException 并返回可理解的结果。
- try仅放可能失败的操作
- catch处理能够恢复的错误
- 失败返回-1不与合法正数混淆
L28Example02.java · 完整独立示例
public class L28Example02 {
static int parseAmount(String text) {
if (text == null) return -1;
try {
int amount = Integer.parseInt(text.strip());
return amount > 0 ? amount : -1;
} catch (NumberFormatException error) { return -1; }
}
public static void main(String[] args) {
System.out.println(parseAmount("12"));
System.out.println(parseAmount("很多"));
}
}运行结果
12
-104 / 15
捕获具体异常
捕获 Exception 虽然省字,却可能把程序错误误装成用户输入问题。优先捕获你能明确处理的具体类型。
- 具体异常NumberFormatException 在前
- 父类异常IllegalArgumentException 在后
- 不可恢复继续传播,别假装成功
L28Example03.java · 完整独立示例
public class L28Example03 {
public static void main(String[] args) {
for (String text : new String[]{"很多", "80"}) {
try {
int amount = Integer.parseInt(text);
if (amount < 1 || amount > 64)
throw new IllegalArgumentException(
"单次数量必须在1到64之间:" + amount);
System.out.println("数量合法=" + amount);
} catch (NumberFormatException error) {
System.out.println("数量必须是整数:" + text);
} catch (IllegalArgumentException error) {
System.out.println(error.getMessage());
}
}
}
}运行结果
数量必须是整数:很多
单次数量必须在1到64之间:8005 / 15
普通验证不必抛异常
玩家输入空名字是常见业务情况,用 if 可以直接表达验证规则。异常用于不能正常完成的操作;选择方式取决于调用合同,而非把异常当作普通分支循环。
- 常见验证空名先用 if
- 清楚合同输入怎样才合法
- 无副作用通过检查才操作
L28Example04.java · 完整独立示例
public class L28Example04 {
static boolean validName(String name) { return name != null && !name.isBlank(); }
public static void main(String[] args) {
System.out.println(validName(" "));
System.out.println(validName(" Alex "));
}
}运行结果
false
true06 / 15
throw 拒绝坏参数
方法若要求单次数量为1到64,就在入口处检查。throw 把错误交给调用者,而不是继续保存坏数据。
- 入口检查参数不合法就拒绝
- throw交给调用者处理
- 消息包含范围与收到的值
L28Example05.java · 完整独立示例
public class L28Example05 {
static void checkAmount(int amount) {
if (amount < 1 || amount > 64)
throw new IllegalArgumentException("数量应为1到64:" + amount);
}
public static void main(String[] args) {
try { checkAmount(-1); }
catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }
checkAmount(64);
System.out.println("64通过");
}
}运行结果
数量应为1到64:-1
64通过07 / 15
finally 与资源清理
收尾要沿着控制路径执行。finally 适合普通收尾;需要关闭的资源优先使用 try-with-resources,避免成功与失败分支各写一遍。
- finally控制路径上的收尾
- 自动资源管理try-with-resources
- 不要 return避免覆盖原异常或结果
L28Example06.java · 完整独立示例
import java.util.*;
public class L28Example06 {
public static void main(String[] args) {
try { Integer.parseInt("很多"); }
catch (NumberFormatException e) { System.out.println("需要整数"); }
finally { System.out.println("本次尝试结束"); }
try (Scanner scanner = new Scanner("3")) {
System.out.println(scanner.nextInt());
}
}
}运行结果
需要整数
本次尝试结束
308 / 15
受检与非受检异常
受检异常会被编译器要求捕获或在方法签名中声明。非受检异常通常表示参数或程序状态不符合合同。
- IOException编译器要求处理或声明
- RuntimeException非受检异常
- 检查方式不同不代表重要性不同
L28Example07.java · 完整独立示例
public class L28Example07 {
static void read() throws java.io.IOException {
throw new java.io.IOException("演示读取失败");
}
public static void main(String[] args) {
try { read(); }
catch (java.io.IOException e) { System.out.println(e.getMessage()); }
}
}运行结果
演示读取失败| 类别 | 例子 | 常见处理 |
|---|---|---|
| 受检异常 | IOException | 捕获或 throws |
| 非受检异常 | IllegalArgumentException | 修正参数合同 |
| 非受检异常 | NumberFormatException | 用户输入可捕获 |
09 / 15
自定义异常表达领域故障
当多个背包规则都可能失败时,自定义异常能让调用者明确知道这是库存领域的问题。
- InventoryException表达一类领域失败
- extends Exception这里设计为受检异常
- 调用者明确选择如何恢复
L28Example08.java · 完整独立示例
class InventoryException extends Exception {
InventoryException(String message) { super(message); }
}
public class L28Example08 {
static int withdraw(int stock, int amount) throws InventoryException {
if (stock < 0 || amount <= 0 || amount > stock)
throw new InventoryException("库存不足");
return stock - amount;
}
public static void main(String[] args) {
try { withdraw(0, 1); }
catch (InventoryException e) { System.out.println(e.getMessage()); }
}
}运行结果
库存不足10 / 15
错误消息要能行动
好消息让使用者知道发生了什么和下一步怎么改,也让程序员能找到对应操作。
- 发生了什么添加操作失败
- 哪个输入物品名或数量
- 下一步提供合法范围与修正建议
| 消息 | 问题 | 改进 |
|---|---|---|
| 错误 | 没有上下文 | 数量必须是整数:很多 |
| 失败了 | 没有操作名 | 添加钻石失败:物品名为空 |
| 范围不对 | 没有合法范围 | 数量必须在1到64之间:80 |
11 / 15
异常路径也要测试
只测试正常输入等于只给盔甲涂颜色。真正的防爆甲要在错误路径上承受冲击。
- 正常操作数量增加
- 解析失败中文数量不能转整数
- 状态比较任何失败都不改库存
L28Example10.java · 第1/2段,顺序接在同一文件
import java.util.*;
public class L28Example10 {
static void addItem(Map<String, Integer> inventory,
String item, int amount) {
if (inventory == null)
throw new IllegalArgumentException("库存不能为null");
if (item == null || item.isBlank())
throw new IllegalArgumentException("物品名不能为空");
if (amount < 1 || amount > 64)
throw new IllegalArgumentException("单次数量必须在1到64之间:" + amount);
String key = item.strip();
Integer old = inventory.get(key);
if (inventory.containsKey(key) && (old == null || old < 0))
throw new IllegalArgumentException("原库存记录不合法:" + key);
long total = (long) (old == null ? 0 : old) + amount;
if (total > Integer.MAX_VALUE)
throw new IllegalArgumentException("累计库存超出整数上限:" + key);
inventory.put(key, (int) total);
}
static String process(Map<String, Integer> inventory,
String item, String amountText) {
if (item == null || item.isBlank()) return "物品名不能为空";
if (amountText == null || amountText.isBlank()) return "数量不能为空";
String key = item.strip();
try {
int amount = Integer.parseInt(amountText.strip());
addItem(inventory, key, amount);
return key + " 添加成功 数量=" + inventory.get(key);L28Example10.java · 第2/2段,顺序接在同一文件
} catch (NumberFormatException error) {
return "数量必须是整数:" + amountText;
} catch (IllegalArgumentException error) {
return error.getMessage();
}
}
public static void main(String[] args) {
Map<String, Integer> inventory = new HashMap<>(Map.of("钻石", 3));
Map<String, Integer> before = new HashMap<>(inventory);
System.out.println(process(inventory, "钻石", "-2"));
System.out.println(inventory.equals(before));
}
}运行结果
单次数量必须在1到64之间:-2
true| 输入 | 路径 | 预期 |
|---|---|---|
| 钻石 3 | 成功 | 数量增加3 |
| 钻石 很多 | 解析失败 | 提示整数 数据不变 |
| 钻石 -2 | 范围失败 | 提示1到64 数据不变 |
| 空名字 5 | 验证失败 | 提示名称 数据不变 |
| 钻石 64 | 边界成功 | 允许添加 |
12 / 15
挑战 蓝图安全背包管理员
挑战 程序叫 SafeInventory。每次操作返回一条友好消息,只有通过全部检查才修改库存。
- process验证文本并转换结果
- addItem维护库存规则
- 写入最后通过全部校验才修改
验收顺序
- 钻石3添加成功,累计数量为3。
- 数量文本“很多”提示必须是整数,库存不变。
- 数量-2提示单次范围1到64,库存不变。
- 空名字在解析之前被拒绝,库存不变。
- 再添加钻石64,累计为67;区分单次上限与累计库存。
13 / 15
安全背包程序
新建 SafeInventory.java,使用 Java 21 编译运行。
- 单次1到64每个请求的数量范围
- 累计库存可超过64,受 int 上限约束
- 友好结果成功或明确拒绝
SafeInventory.java · 第1/2段,顺序接在同一文件
import java.util.*;
public class SafeInventory {
static void addItem(Map<String, Integer> inventory,
String item, int amount) {
if (inventory == null)
throw new IllegalArgumentException("库存不能为null");
if (item == null || item.isBlank())
throw new IllegalArgumentException("物品名不能为空");
if (amount < 1 || amount > 64)
throw new IllegalArgumentException("单次数量必须在1到64之间:" + amount);
String key = item.strip();
Integer old = inventory.get(key);
if (inventory.containsKey(key) && (old == null || old < 0))
throw new IllegalArgumentException("原库存记录不合法:" + key);
long total = (long) (old == null ? 0 : old) + amount;
if (total > Integer.MAX_VALUE)
throw new IllegalArgumentException("累计库存超出整数上限:" + key);
inventory.put(key, (int) total);
}
static String process(Map<String, Integer> inventory,
String item, String amountText) {
if (item == null || item.isBlank()) return "物品名不能为空";
if (amountText == null || amountText.isBlank()) return "数量不能为空";
String key = item.strip();
try {
int amount = Integer.parseInt(amountText.strip());
addItem(inventory, key, amount);
return key + " 添加成功 数量=" + inventory.get(key);SafeInventory.java · 第2/2段,顺序接在同一文件
} catch (NumberFormatException error) {
return "数量必须是整数:" + amountText;
} catch (IllegalArgumentException error) {
return error.getMessage();
}
}
public static void main(String[] args) {
Map<String, Integer> inventory = new HashMap<>();
System.out.println(process(inventory, "钻石", "3"));
System.out.println(process(inventory, "钻石", "很多"));
System.out.println(process(inventory, "钻石", "-2"));
System.out.println(process(inventory, " ", "5"));
System.out.println(process(inventory, "钻石", "64"));
System.out.println("最终库存=" + inventory.get("钻石"));
}
}运行结果
钻石 添加成功 数量=3
数量必须是整数:很多
单次数量必须在1到64之间:-2
物品名不能为空
钻石 添加成功 数量=67
最终库存=6714 / 15
拓展任务与通关复盘
作业同时提交成功路径和至少三个失败路径。每个 catch 都写一句为什么能正确恢复。
- 成功路径至少一次真实添加
- 失败路径解析、范围、空名
- catch 理由解释为何能够恢复
三档作业
- 木镐:安全读取年龄,拒绝非整数和负数。
- 铁镐:解析x y z坐标,任何一项非法时保留旧位置。
- 钻石镐:记录每种错误出现次数,并在最后输出诊断报告。
| 自评 | 能 | 还要练 |
|---|---|---|
| 我会读异常线索 | □ | □ |
| 我会捕获具体异常 | □ | □ |
| 我能测试失败后不变 | □ | □ |
15 / 15
自己的验收记录
漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。
| 类别 | 你选择的操作 | 核对重点 |
|---|---|---|
| 正常 | 一个常用输入或一次成功动作 | 结果是否逐行一致 |
| 边界 | 范围端点与相邻值 | 有没有多一步、少一项 |
| 错误 | 非法数量、缺失对象或坏数据 | 是否明确拒绝,状态是否保持 |
- Bug 记录:触发条件、实际现象、原因、最小修复。
- 图示记录:为每条箭头说明起点、终点和含义。
- 改编记录:只改一个规则,同时修改预期和测试。
TAKE IT WITH YOU
下载本课 PDF
完整课文、代码、图解与练习,选择你需要的语言。