← 返回 Java 后端知识路线
阶段 01Java 基础

分支、循环与调试:让程序在边界上仍然可解释

用票务计数器拆解条件、循环、断点和日志,练习从错误输出反推状态变化,而不是凭感觉改代码。

第 03 / 35 篇
if循环断点调试

先看这一课值不值得学

学完后,你手里多了哪些代码积木

让程序根据条件选择路线,并重复执行可观察的步骤。

本课正式新增

语法 / API / 命令你必须会到什么程度
if / else if / else按 boolean 条件选择一个分支
switch / case / default按离散值选择分支
for / while / do...while按次数或条件重复执行
break / continue结束循环或跳过本轮

本课只借用,先别硬背

  • IDEA 断点和变量窗口是调试工具,不是 Java 语法

学完必须能独立写

  • 输入成绩输出等级
  • 统计一批数据并用断点解释每轮变量变化
本课目录
  1. 1. 现实问题:少卖一张票也可能是状态设计错了
  2. 2. 最小可运行示例:把状态变化打印出来
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:把控制流拆进可测试的业务方法
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:少卖一张票也可能是状态设计错了

假设售票窗口接收若干笔购买数量,库存不能变成负数,超过剩余库存的订单要被拒绝。最初的代码通常是几个嵌套 if 加一个 for,但真正困难的是:循环每一轮结束时库存应该满足什么条件?如果不先写出这个不变量,调试只会变成在不同符号之间来回试。

控制流学习的目标不是把所有语法写一遍,而是能回答“条件什么时候判断、状态什么时候变化、循环什么时候停止”。这三个问题能迁移到分页、重试、权限判断和订单状态机。

2. 最小可运行示例:把状态变化打印出来

public class TicketCounter {
    public static void main(String[] args) {
        int stock = 7;
        int[] requests = {2, 3, 4, 1};
        int sold = 0;

        for (int request : requests) {
            if (request <= 0) {
                System.out.println("忽略非正购买量:" + request);
                continue;
            }
            if (request > stock) {
                System.out.printf("拒绝 %d 张,剩余 %d 张%n", request, stock);
                continue;
            }
            stock -= request;
            sold += request;
            System.out.printf("成交 %d 张,库存=%d,已售=%d%n", request, stock, sold);
        }
        System.out.println("最终库存:" + stock);
    }
}

输入数组里第四笔虽然合法,但前面的库存已经不足以满足第三笔,所以它仍然会按剩余状态判断。continue 让失败分支提前结束本轮,成功分支的扣减逻辑只出现一次。若规则要求售票直到库存为空,可以换成 while (stock > 0),但一定要确认每轮都能让 stock 变小,否则会产生死循环。

3. 调用链与对象变化

程序创建一个 int[] 数组,for-each 每一轮从数组取出一个基本类型值复制到 requeststocksold 存在栈帧的局部变量槽中,比较只读取它们,只有通过校验后才执行减法和加法。printf 读取当前快照并产生输出,不会反向改变库存。

调试时可以在 if (request > stock) 前设置断点,观察四个量:当前索引、request、stock、sold。第一轮后不变量是 stock + sold == 7;如果断点发现这个等式被破坏,就不用继续看输出格式,而应该回到状态更新的两行。对象没有参与这里的数值传递,但数组本身是引用,方法若接收该数组并修改元素,调用方会看到修改后的内容。

4. 为什么这样设计

分支的排列应先处理非法输入,再处理业务拒绝,最后执行成功路径,这叫“守卫式判断”。它减少嵌套层级,让成功路径保持直线。continue 适合跳过一个独立请求,break 则表示整个任务已经没有继续价值;二者不能仅凭“少写几行”互换。

循环要有三件事:初始状态、继续条件、每轮进展。计数循环关注索引是否越界,条件循环关注状态是否收敛,遍历集合则关注迭代器是否已经到末尾。把循环不变量写在注释或测试名称里,比在循环体里堆日志更有价值。

5. 项目落点:把控制流拆进可测试的业务方法

真正的售票服务不应在 main 里读输入、改库存和打印消息。可以把 trySell(int request) 放进 TicketInventory,返回一个包含是否成功、剩余库存和原因的结果对象;Controller 只把 HTTP 参数交给 Service。这样可以直接测试“库存 0、请求 0、请求超过库存、连续成功”四类状态,而不依赖控制台。

调试习惯也要进入项目:本地用断点查看对象,线上用结构化日志记录订单号、请求量、变更前后库存和结果,不记录密码与完整令牌。日志是观察,不是业务状态;不要为了打印而在循环里触发额外数据库查询。

6. 易错排查

  • 循环永不结束:检查条件依赖的变量是否在每轮变化,尤其是 while 里的计数器。
  • ArrayIndexOutOfBoundsException:先确认索引范围是 0 <= i < array.length,不要用固定数字代替长度。
  • 库存变成负数:扣减前判断,且判断和扣减必须位于同一业务边界;多线程场景还要加原子性。
  • 断点没有停:确认运行的是最新编译产物,调试配置没有连到另一份 class 或另一台进程。

7. 一页复习

先写状态和不变量,再写分支和循环;非法输入提前返回,成功路径保持单一;每轮循环必须有可证明的进展。调试不是“逐行看完所有代码”,而是选一个不变量,找到它第一次失真的位置。这个方法会在数据库事务、缓存过期和并发竞态中继续使用。

把四轮请求列成逐步状态表:开始时 stock=7,sold=0;处理 2 后是 5,2;处理 3 后是 2,5;请求 4 被拒绝仍是 2,5;请求 1 后是 1,6。表格说明了拒绝分支不能修改状态,也说明“打印了拒绝”不是证明库存正确,最终不变量仍需单独检查。若实际输出出现 stock=0,sold=7,就继续追问是哪一轮改变了它,而不是凭视觉猜循环条件。

在项目文件里可以把 TicketInventory.trySell 设计成返回 SaleResult,其中包含 acceptedrequestedremainingreason。Controller 将 reason 翻译成 400/409,库存对象只负责状态;命令行 demo 负责打印。这样调试器观察的是结果对象,日志也能携带订单号和扣减前后值,不必从一段自然语言中反推状态。

断点和日志各有位置:断点适合一次性观察循环变量,条件断点适合只停在 request > stock,日志适合多次运行和并发环境。若断点看到数组内容正确但输出错,检查 printf 使用的变量;若第三轮开始状态就错,检查第二轮成功分支的两次更新;若程序卡住,先看 while 的进展变量是否被 continue 跳过。

练习是把请求改成来自文件的多行文本,并为每个请求生成一个序号。解析失败的行不应改变库存,但要在摘要中计数;合法但库存不足的行也不应被当成解析失败。最后写一个断言验证 sold + stock 等于初始库存,测试空文件、负数、超库存和一条成功一条失败的混合输入。

循环不变量也可以写成测试断言:每次成功卖出后 stock >= 0sold >= 0,且 stock + sold 等于初始库存;每次拒绝后库存和已售数都不变。断言放在状态更新后,能精确定位破坏发生的轮次。若未来改为按订单对象处理,再把订单号加入状态快照,避免两个请求的日志混在一起。

项目里可以把控制流拆成 parseRequesttrySellsummarize 三个文件或方法:解析阶段只产生合法请求,库存阶段只改变状态,摘要阶段只读取结果。这样一个循环不再同时承担 IO、业务和展示。调试时先复现相同输入,再比较每一步状态;只有输入和环境相同,断点看到的差异才有意义。

常见错误还包括把 break 当成“拒绝本轮”、把 continue 放在库存扣减之前的错误位置,以及循环里修改集合导致迭代器异常。修复前分别写出这三个关键字的预期控制流图,确认是结束本轮、结束整个循环还是进入下一阶段。练习完成后把状态摘要输出为 CSV,下一节文件 IO 可以直接复用。

调试报告可以保留“输入数组、每轮状态、最终不变量、返回码”四项。它比只贴最后一行输出更能说明错误在哪里第一次出现,也能让后续把控制台程序改成 Service 时沿用同一组断言。

验证清单:把请求序列 2、3、4、1、负数、空行分别送入程序,逐轮记录输入、解析结果、stock、sold、accepted 和 reason;解析失败、库存不足和成功扣减必须是三种不同输出。运行前保存初始不变量,运行后断言 stock 不为负且 stock+sold 等于初始库存;再打开条件断点只观察 stock 小于 request 的轮次。复盘时把 break、continue、return 在当前循环中的作用画出来,并检查调试输出本身没有修改状态,避免用“看起来打印正确”代替可重复的验证。

进阶附录:断言与条件断点

开发期可以写 assert stock >= 0 : stock;,但生产环境默认可能关闭断言,所以不能用它替代业务校验。IDEA 的条件断点可以只在 stock < request 时暂停,日志也可以使用采样或限频,避免调试手段反过来改变时序和性能。

本课按「Java 21 控制流、循环不变量与基础调试」的学习范围组织,正文与示例均为本站原创整理。