DEV Community

Homlander
Homlander

Posted on

07 Java 调试案例讲解

一、调试究竟在做什么

调试(Debugging)就是查找并改正程序错误的过程。 核心是观察程序的实际执行过程,逐步缩小问题范围,找到“从哪一步开始,实际结果与预期不一致”。

常见方式有两种:

  • 阅读代码、添加打印语句:代码较少时,按执行顺序检查,或用 System.out.println() 输出中间结果。
  • 使用调试器:让程序在指定位置停下来,查看变量,再控制它一步步执行。代码较复杂、需要反复观察时更方便。

以下使用 IntelliJ IDEA,Java 环境沿用课件中的 JDK 17。快捷键按课件的 Windows 键位说明;若自行修改过键位,以按钮提示为准。

二、先让程序在断点处停下来

1. 准备示例

新建 Test.java,后面的操作都围绕这段代码展开:

public class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int ret = add(a, b);       // 断点 A
        System.out.println(ret);  // 断点 B,后面再添加
    }

    public static int add(int x, int y) {
        System.out.println("进入 add");
        return x + y;
    }
}
Enter fullscreen mode Exit fullscreen mode

正常运行时输出:

进入 add
30
Enter fullscreen mode Exit fullscreen mode

2. 设置断点并启动 Debug

断点是告诉调试器“执行到这里先停一下”的标记。

  1. 在 int ret = add(a, b); 所在行的行号旁边单击,出现红点,即设置了断点 A。
  2. 在编辑区右键,选择 Debug 'Test.main()'。
  3. 程序运行到断点 A 时暂停,下方出现 Debug 窗口。

再次单击红点可以删除该断点。要让断点起作用,应使用 Debug 启动。

3. 看懂“停在这一行”

刚命中行断点时,该行通常还没有执行。

本例停在断点 A 时:

  • a = 10、b = 20 已执行,可以查看它们的值。
  • add(a, b) 尚未执行,ret 还没有完成初始化。
  • 控制台还没有输出“进入 add”和 30。

红点表示断点位置,执行高亮表示程序当前停留的位置。判断某个结果为什么还没出现,先检查产生结果的语句是否已经执行。

三、暂停后先看什么

1. Debugger:看变量和调用堆栈

变量区显示当前选中方法中的变量及其值。例如,停在 main 中可以查看 a、b;进入 add 后,可以查看 x、y。

调用堆栈表示当前有哪些方法正在调用、等待返回。例如:

add:行号
main:行号
Enter fullscreen mode Exit fullscreen mode

表示 main 调用了 add,当前正在执行 add,main 正在等待它返回。最上方通常是当前执行的方法,下面是调用它的方法。

点击不同的方法记录,可以查看该次调用对应的变量。后面的数字是代码行号,不是调用次数。

2. 表达式求值与监视

程序暂停时,在 Debugger 中的 Evaluate expression ... or add a watch 输入区域填写表达式,按回车可查看结果。

例如停在 add 方法内部、x = 10、y = 20 时:

x + y        // 结果:30
(x + y) * 2  // 结果:60
Enter fullscreen mode Exit fullscreen mode
  • 求值:临时计算一次,检查当前结果。
  • 监视(Watch):把表达式加入监视列表,后续暂停时方便反复查看。

表达式中的变量必须在当前查看的位置可用。例如选中 add 时使用 x、y,选中 main 时使用 a、b。

3. Console:看输入输出

Console 显示控制台的输入输出,Debugger 用来观察执行状态。

只有输出语句执行之后,Console 才会出现对应内容。程序停在 System.out.println(ret); 这一行时,并不表示 30 已经打印。

四、用单步操作控制执行过程

1. 先区分四个操作

操作 快捷键 作用
Step Over(步过/逐过程) F8 执行当前行;遇到方法调用时,通常不进入方法内部逐步查看
Step Into(步入/逐语句) F7 进入当前行调用的方法,查看内部执行过程
Force Step Into(强制步入) Alt + Shift + F7 进入普通步入可能因过滤规则而跳过的方法
Step Out(步出/跳出) Shift + F8 执行完当前方法剩余部分,返回调用它的位置

2. 用同一段代码理解 F8、F7 和 Shift + F8

情况一:只关心方法的返回结果,用 F8。

停在断点 A 时按 F8,add(a, b) 会正常执行。到达下一行 System.out.println(ret); 时:

  • ret = 30。
  • Console 已输出“进入 add”。
  • 30 还没输出,再执行当前行才会输出。

步过只是通常不进入方法内部逐步查看,方法本身仍然执行。 如果方法内部另有断点,也可能在内部停下来。

情况二:想检查方法内部的计算过程,用 F7。

重新调试,在断点 A 按 F7:

  1. 进入 add,停在 System.out.println("进入 add");。
  2. 变量区可以看到 x = 10、y = 20。
  3. 按 F8,执行打印语句,停在 return x + y;。
  4. 此时可以求值 x + y,确认结果为 30。
  5. 继续执行返回语句,返回 main;调用及赋值完成后,ret 才得到 30。

情况三:已经进入方法,但不想继续逐行看,用 Shift + F8。

如果刚进入 add 就使用步出,剩余的打印和 return 仍会执行,然后回到 main 的调用位置。

步出不会取消这次调用,也不会丢掉剩余代码。 返回后界面可能仍高亮调用所在行,继续单步即可观察后续赋值和输出。

3. 强制步入用在什么地方

普通 F7 可能按调试器的过滤规则跳过某些 Java 类库方法,例如 System.out.println() 的内部实现。确实需要查看这些方法时,可以使用 Alt + Shift + F7。

“强制”是指绕过普通步入的过滤规则,并不意味着任何方法都一定有可供查看的源码。本章只需掌握它与普通步入的区别。

上述步进操作的含义可对照 IDEA 官方说明。

五、不想逐行执行时怎么做

1. 继续运行到下一个断点:F9

在 System.out.println(ret); 这一行再设置断点 B。

程序停在断点 A 时,按 F9(Resume Program),程序继续正常执行,命中断点 B 后暂停。此时 ret = 30,但 30 尚未打印。

如果后续没有命中其他断点,程序就会继续运行,直到结束或因其他原因暂停。

“到下一个断点”按实际执行顺序判断,不是按编辑器中红点的上下顺序判断。

2. 运行到光标处:Alt + F9

只想临时运行到某一行,可以:

  1. 在程序暂停时,把编辑光标放到目标代码行。
  2. 按 Alt + F9(Run to Cursor)。

例如停在断点 A 时,把光标放到 System.out.println(ret);,执行后会在这行暂停,便于查看 ret。

目标位置之前的代码仍会执行;如果途中先命中其他断点,会先在那个断点暂停。目标行也必须是本次执行能够到达的位置。

3. 重新调试与停止

操作 快捷键 含义
Rerun(重新调试) Ctrl + F5 从头重新启动当前调试配置,适合重新观察执行过程
Stop(停止) Ctrl + F2 终止当前程序,结束这次运行

Ctrl + F2 是停止,不是可继续执行的暂停。 停止之后,要再次运行就需要重新启动。参见 IDEA 调试会话说明。

六、管理断点与设置条件断点

1. 查看、禁用和屏蔽断点

  • View Breakpoints:Ctrl + Shift + F8。集中查看、编辑、启用、禁用或删除断点。
  • Mute Breakpoints(屏蔽断点):在 Debug 工具栏点击该按钮,临时屏蔽所有断点;再次点击可解除屏蔽。断点仍然保留,不需要重新添加。

禁用适合暂时保留某个断点,删除适合不再需要的断点,屏蔽适合临时让程序不因这些断点而停下。

2. 条件断点:只在关心的那一次暂停

循环执行很多次时,普通断点会反复暂停。如果只想检查 i == 20 的情况,可以设置条件断点。

把 main 方法中的代码替换为:

for (int i = 0; i < 100; i++) {
    System.out.println(i); // 在这一行设置断点
}
Enter fullscreen mode Exit fullscreen mode

操作步骤:

  1. 在 System.out.println(i); 行设置断点。
  2. 右键单击断点红点,在 Condition 中填写 i == 20。
  3. 启动 Debug。

程序会正常执行前面的循环,直到 i == 20 时,才在该断点暂停。此时:

  • Debugger 中的 i 为 20。
  • Console 已输出 0 到 19。
  • 20 还没有输出,因为当前打印语句尚未执行。

条件应是结果为 true 或 false 的表达式。这里写 i == 20,不是赋值表达式 i = 20,也不需要写 if 或分号。

条件断点只控制是否暂停,不会让条件不满足时的代码停止执行。

七、把调试操作用来定位错误

假设加法示例中的返回语句误写成:

return x - y;
Enter fullscreen mode Exit fullscreen mode

预期结果是 30,实际却输出 -10。可以这样定位:

  1. 在 int ret = add(a, b); 处打断点并 Debug。
  2. 查看 a = 10、b = 20,确认调用前的数据正确。
  3. 按 F7 进入 add,确认 x = 10、y = 20,参数传递也正确。
  4. 执行到返回语句,对比预期的 x + y 与实际代码 x - y,发现运算符写错。
  5. 改为 return x + y;,重新调试,确认 ret = 30,最终输出正确。

调试时每一步都应带着一个检查目标:输入是否正确、哪一步计算有误、返回结果是否符合预期。 找到第一个出现偏差的位置,通常就能明显缩小错误范围。

Top comments (0)