← Java 基础教程

第 28 / 40 课 · 免费教程

给程序穿防爆甲

读懂异常,让失败也有清楚的结果

100–120 分钟本章使用 JDK 21,无需预览功能。集合与程序实践

本课作品

制作安全背包管理员 SafeInventory

用户输入“很多”,程序不必立刻结束。你会读懂异常线索,捕获能够恢复的失败,在写入库存前检查全部条件,并给出可行动的提示。 前置背包:方法、条件判断、Map 和字符串转换。今天玩家输入很多颗钻石时,程序不会像苦力怕一样原地爆炸。

开始前:建议先完成第 25–27 课

类型 消息和调用位置组成故障线索
  1. 异常类型说明哪类操作失败
  2. 消息给出具体输入线索
  3. 源文件行号定位自己的代码

01 / 15

开始前:先预测,再让程序回答

本章使用 JDK 21,无需预览功能。故事借用方块世界,示例运行在普通 Java 控制台;数值和规则若是课程模型,都会明确说明。

每个普通示例都是完整独立文件,按框上标注的文件名保存。各示例放在不同文件夹,避免辅助类重名。完整项目分段时,按顺序接在同一个文件中。

终端命令:中文占位符换成实际文件名与类名

javac -encoding UTF-8 示例文件名.java
java 示例类名
  • 预测:先写预期输出,或画清引用和数据的变化。
  • 验证:运行后逐行核对,再只改一个条件。
  • 解释:说清结果来自 Java 规则、课程合同还是输入数据。

02 / 15

异常报告失败的操作

异常不是程序员被扣分的通知,而是运行时发现操作无法正常完成后送出的故障报告。

堆栈跟踪会列出方法调用路径。先看异常类型和消息,再找第一条指向自己源文件的行。

类型 消息和调用位置组成故障线索
  1. 异常类型说明哪类操作失败
  2. 消息给出具体输入线索
  3. 源文件行号定位自己的代码

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. try仅放可能失败的操作
  2. catch处理能够恢复的错误
  3. 失败返回-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
-1

04 / 15

捕获具体异常

捕获 Exception 虽然省字,却可能把程序错误误装成用户输入问题。优先捕获你能明确处理的具体类型。

不同故障需要不同修理方案
  1. 具体异常NumberFormatException 在前
  2. 父类异常IllegalArgumentException 在后
  3. 不可恢复继续传播,别假装成功

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之间:80

05 / 15

普通验证不必抛异常

玩家输入空名字是常见业务情况,用 if 可以直接表达验证规则。异常用于不能正常完成的操作;选择方式取决于调用合同,而非把异常当作普通分支循环。

可预期选择用 if 处理 真正失败再交给异常
  1. 常见验证空名先用 if
  2. 清楚合同输入怎样才合法
  3. 无副作用通过检查才操作

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
true

06 / 15

throw 拒绝坏参数

方法若要求单次数量为1到64,就在入口处检查。throw 把错误交给调用者,而不是继续保存坏数据。

方法发现合同被破坏就立即报告
  1. 入口检查参数不合法就拒绝
  2. throw交给调用者处理
  3. 消息包含范围与收到的值

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,避免成功与失败分支各写一遍。

正常返回或抛出异常时,沿控制路径收尾
  1. finally控制路径上的收尾
  2. 自动资源管理try-with-resources
  3. 不要 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());
        }
    }
}

运行结果

需要整数
本次尝试结束
3

08 / 15

受检与非受检异常

受检异常会被编译器要求捕获或在方法签名中声明。非受检异常通常表示参数或程序状态不符合合同。

编译器要求处理部分可恢复故障
  1. IOException编译器要求处理或声明
  2. RuntimeException非受检异常
  3. 检查方式不同不代表重要性不同

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 说明背包操作失败
  1. InventoryException表达一类领域失败
  2. extends Exception这里设计为受检异常
  3. 调用者明确选择如何恢复

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. 发生了什么添加操作失败
  2. 哪个输入物品名或数量
  3. 下一步提供合法范围与修正建议
验收对照表
消息问题改进
错误没有上下文数量必须是整数:很多
失败了没有操作名添加钻石失败:物品名为空
范围不对没有合法范围数量必须在1到64之间:80

11 / 15

异常路径也要测试

只测试正常输入等于只给盔甲涂颜色。真正的防爆甲要在错误路径上承受冲击。

成功 捕获 抛出和状态不变都要验收
  1. 正常操作数量增加
  2. 解析失败中文数量不能转整数
  3. 状态比较任何失败都不改库存

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。每次操作返回一条友好消息,只有通过全部检查才修改库存。

输入处理:先检查,后修改
  1. process验证文本并转换结果
  2. addItem维护库存规则
  3. 写入最后通过全部校验才修改

验收顺序

  • 钻石3添加成功,累计数量为3。
  • 数量文本“很多”提示必须是整数,库存不变。
  • 数量-2提示单次范围1到64,库存不变。
  • 空名字在解析之前被拒绝,库存不变。
  • 再添加钻石64,累计为67;区分单次上限与累计库存。

13 / 15

安全背包程序

新建 SafeInventory.java,使用 Java 21 编译运行。

具体异常得到具体提示
  1. 单次1到64每个请求的数量范围
  2. 累计库存可超过64,受 int 上限约束
  3. 友好结果成功或明确拒绝

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
最终库存=67

14 / 15

拓展任务与通关复盘

作业同时提交成功路径和至少三个失败路径。每个 catch 都写一句为什么能正确恢复。

三档任务继续强化错误处理
  1. 成功路径至少一次真实添加
  2. 失败路径解析、范围、空名
  3. catch 理由解释为何能够恢复

三档作业

  • 木镐:安全读取年龄,拒绝非整数和负数。
  • 铁镐:解析x y z坐标,任何一项非法时保留旧位置。
  • 钻石镐:记录每种错误出现次数,并在最后输出诊断报告。
验收对照表
自评能还要练
我会读异常线索□□
我会捕获具体异常□□
我能测试失败后不变□□

15 / 15

自己的验收记录

漂亮的输出是起点,能解释边界才算掌握。把正常、边界与错误各选一个案例,运行前写预期,运行后留下证据。

为判断留下依据
类别你选择的操作核对重点
正常一个常用输入或一次成功动作结果是否逐行一致
边界范围端点与相邻值有没有多一步、少一项
错误非法数量、缺失对象或坏数据是否明确拒绝,状态是否保持
  • Bug 记录:触发条件、实际现象、原因、最小修复。
  • 图示记录:为每条箭头说明起点、终点和含义。
  • 改编记录:只改一个规则,同时修改预期和测试。

TAKE IT WITH YOU

下载本课 PDF

完整课文、代码、图解与练习,选择你需要的语言。