Skip to content

Java 程序的运行与调试 ​

S0-005必学入门约 60 分钟JDK 21学习准备与环境 · 环境与工具链

1. 本节定位 ​

《IDEA 与项目创建》 里你已经用过一次断点。本节把调试从"会用"变成"会用来解决问题":拆解程序运行的三个阶段,掌握四种断点与四个单步操作,学会用调用栈理解递归。

调试能力是程序员的分水岭:不会调试的人靠猜和 System.out.println,会调试的人能直接看到程序内部发生了什么。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出 Java 程序从源码到运行的三个阶段分别在做什么;
  • 熟练使用四种断点:行断点、条件断点、异常断点、方法断点;
  • 分清步过(Step Over)/ 步入(Step Into)/ 步出(Step Out)/ 继续(Resume)的区别;
  • 看懂调用栈,并用它理解递归的执行过程;
  • 用监视(Watch)与求值(Evaluate Expression)检查复杂表达式的值。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

System.out.println 调试有三个硬伤:

问题具体表现
看不到中间状态一个表达式套三层调用,你只能打印最终结果,看不到中间值
必须改代码每换一个观察点就要加一行打印、重新运行
处理不了时机问题循环第 873 次出错、只有特定参数才复现——打印会淹没在输出里

断点调试解决这三点:随时暂停、随时看值、随时继续,而且不改动代码。

5. 核心概念 ​

5.1 程序运行的三个阶段 ​

text
① 编译期(javac)      源码 .java ──▶ 字节码 .class        检查语法与类型
② 加载期(类加载器)    .class ──▶ JVM 内存中的 Class 对象   加载、链接、初始化
③ 运行期(JVM 执行)    执行字节码,方法调用形成调用栈        异常在这里抛出

这个划分能解释很多现象:

  • 编译错误是①的问题,IDE 会直接标红,程序根本跑不起来;
  • ClassNotFoundException 是②的问题,通常是依赖没打进去;
  • NullPointerException、逻辑错误是③的问题,只有调试才能看清。

5.2 四种断点 ​

断点类型怎么下解决什么问题
行断点行号旁点击常规暂停
条件断点右键行断点 → 填 Condition只在 i == 873 或 userId == null 时停下
异常断点调试面板 → View Breakpoints → Java Exception Breakpoint谁抛的异常、在哪抛的,一停就知道
方法断点在方法签名行下断点观察某个方法被谁调用、参数是什么

条件断点是最被低估的功能:调试循环问题时,它能让你直接跳到出问题的那一次迭代。

5.3 四个单步操作 ​

快捷键动作作用
F8步过 / Step Over执行当前行,不进入被调方法(把调用当成一步)
F7步入 / Step Into进入被调方法内部(要看清实现时用)
F9继续 / Resume Program继续运行到下一个断点
⇧F8步出 / Step Out从当前方法跳出,回到调用处

配套的常用键位:

快捷键动作作用
Alt+F9运行到光标处 / Run to Cursor一路跑到光标所在行,中途遇到断点照样停
Alt+F8求值 / Evaluate Expression暂停时执行任意表达式,不改代码
Ctrl+F8切换行断点 / Toggle Line Breakpoint断点的开关
Ctrl+Shift+F8查看断点 / View Breakpoints打开断点管理窗口(含异常断点)
⇧F9 / ⇧F10调试 / 运行启动以调试 / 运行模式启动当前配置
Ctrl+F2停止 / Stop结束调试会话

键位以你本机 IDEA 为准

上表是 Windows 默认 keymap。若你切换过 Keymap 方案(如 Eclipse)或改过键位,键位会变——按 Ctrl+Shift+A 打开动作搜索,右侧就会显示当前键位。

选择原则:关心被调方法的内部用步入(F7 / Step Into),只关心它的结果用步过(F8 / Step Over)。全都步入会在框架代码里迷路(所以 IDEA 默认会跳过 JDK 与框架的源码)。

5.4 调用栈:理解递归的钥匙 ​

每进入一个方法,JVM 就在调用栈上压入一个栈帧(保存参数、局部变量、返回地址);方法返回时弹出。

text
factorial(1)     ← 栈顶(当前正在执行)
factorial(2)
factorial(3)
factorial(4)
factorial(5)
main             ← 栈底

递归难理解,是因为它的执行顺序是"先进后出":一路压栈到最深处,再从最深处一路返回。在 IDEA 的 Frames 面板里看这个过程,比看十遍文字描述都清楚。

6. 递进示例 ​

6.1 最小示例:行断点看循环 ​

java
int sum = 0;
for (int i = 1; i <= 5; i++) {
    sum += i;          // 在这里下断点,每次循环都停
}

反复按 F8(步过 / Step Over),观察 i 与 sum 的同步变化。

6.2 业务示例:用调用栈理解递归 ​

java
static long factorial(int n) {
    if (n <= 1) {
        return 1;                 // 递归出口
    }
    return n * factorial(n - 1);  // 递归调用
}

调试步骤:

  1. 在 return n * factorial(n - 1); 下断点;
  2. 反复按 F7(步入 / Step Into),看 Frames 面板从 factorial(5) 一层层堆到 factorial(1);
  3. 到 n == 1 时,改按 F8(步过 / Step Over),观察 Frames 面板的栈帧从顶部逐层弹出;
  4. 注意 Variables 面板:每一层的 n 是独立的,互不干扰——这是理解"每次调用都有自己的局部变量"的最好方式。

6.3 反例:用打印调试并发问题 ​

java
// 想排查为什么结果不是 200000
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        for (int j = 0; j < 20000; j++) {
            System.out.println("加一前:" + counter);   // 打印让时序更难复现
            counter++;
        }
    }).start();
}

打印本身会改变线程的执行时序,把"偶发问题"变成"时而复现时而不复现"。这类问题要用断点(配合 Suspend 策略设为 Thread)或日志框架,而不是 println。

7. 完整可运行代码 ​

文件:labs/start/src/main/java/cn/kb/learn/DebugPractice.java

java
package cn.kb.learn;

import java.util.ArrayList;
import java.util.List;

public class DebugPractice {
    public static void main(String[] args) {
        System.out.println("阶乘 5 = " + factorial(5));
        System.out.println("斐波那契前 8 项 = " + fibonacci(8));
        System.out.println("奇数 = " + oddNumbers(List.of(1, 2, 3, 4, 5, 6, 7)));
    }

    static long factorial(int n) {
        if (n <= 1) {
            return 1;
        }
        return n * factorial(n - 1);
    }

    static List<Integer> fibonacci(int count) {
        List<Integer> result = new ArrayList<>();
        int a = 0;
        int b = 1;
        for (int i = 0; i < count; i++) {
            result.add(a);
            int next = a + b;
            a = b;
            b = next;
        }
        return result;
    }

    static List<Integer> oddNumbers(List<Integer> numbers) {
        List<Integer> odds = new ArrayList<>();
        for (Integer number : numbers) {
            if (number % 2 != 0) {
                odds.add(number);
            }
        }
        return odds;
    }
}

运行命令:

powershell
javac --release 21 -d start/target/classes start/src/main/java/cn/kb/learn/DebugPractice.java
java -cp start/target/classes cn.kb.learn.DebugPractice

预期输出:

text
阶乘 5 = 120
斐波那契前 8 项 = [0, 1, 1, 2, 3, 5, 8, 13]
奇数 = [1, 3, 5, 7]

8. Debug 观察点 ​

按顺序做完这四件事,你就掌握了本节的核心:

  1. 行断点:在 result.add(a); 下断点,用 F8(步过 / Step Over) 走 8 次,看 a、b、next 三个变量如何"滚动"赋值;
  2. 条件断点:把断点条件设为 i == 5,重新调试,观察程序是否直接停在第六次迭代(不用手点 5 次步过);
  3. 方法断点:在 factorial 的方法签名行下断点,重新调试,看每次进入方法时 n 的值,配合 Frames 面板观察栈的深度变化;
  4. 求值(Alt+F8 / Evaluate Expression):调试中选中 numbers.size() - 1 后触发求值,或者输入任意表达式(如 numbers.stream().filter(x -> x > 4).count())——调试时能执行任意代码,这是排查复杂条件的最快方式。

9. 常见错误 ​

错误 1:断点打了但不停 ​

原因:代码是旧版本(IDE 没重新编译)、断点被禁用(灰色)、或者调试启动的不是当前模块。修复:先 Build → Rebuild Project,再看断点是否为红色实心。

错误 2:条件断点写成赋值 ​

java
i = 5      // 错误:这是赋值,等于把 i 改成 5
i == 5     // 正确:这才是判断

条件断点的输入框接受的是表达式,写错不报错但行为完全不对。

错误 3:调试时修改代码导致断点错位 ​

原因:热交换(HotSwap)只能加方法体,不能改签名。改得多了断点位置会与实际代码不匹配。修复:停止调试 → 重新编译 → 重新开始。

错误 4:只在 main 里调试,不敢进方法 ​

修复:大胆用 F7(步入 / Step Into) 进入自己的方法;IDEA 默认会跳过 JDK 与第三方库的源码(可在 Settings → Debugger → Stepping 里调整),所以不会"迷路"到 HashMap 内部。

10. 动手实验 ​

目标:用条件断点 + 调用栈定位一个"只在特定数据下出错"的问题。

步骤:

  1. 把第 7 节的代码跑一遍,确认输出一致;
  2. 给 oddNumbers 传一个包含 null 的列表(如 Arrays.asList(1, null, 3)),运行并观察异常;
  3. 用异常断点(View Breakpoints → Java Exception Breakpoint → 搜 NullPointerException)重新调试,让程序停在抛异常的精确位置;
  4. 在停下时的 Frames 面板里,回答:异常是从哪个方法抛出的?它的调用者是谁?
  5. 用 factorial 做一次 F7(步入 / Step Into) 连按,数一数栈最深有几层。

验收方式:能说出第 4 步两个问题的答案,并解释第 5 步的层数与 n 的关系。

扩展任务:把 oddNumbers 改成流式写法(numbers.stream().filter(n -> n % 2 != 0).toList()),在 lambda 里下断点,观察 lambda 的调用栈与普通方法有什么不同。

11. 练习题 ​

基础题

  1. 判断:步过(F8 / Step Over)会进入被调用的方法内部。(对 / 错)
  2. 填空:要看一个方法被谁调用,可以下 ______ 断点。
  3. 简答:为什么说"打印会改变程序的行为"?在什么场景下这最致命?

代码阅读题

java
static int mystery(int n) {
    if (n == 0) return 0;
    return n + mystery(n - 1);
}
  1. mystery(4) 返回多少?请描述调试时 Frames 面板会依次出现哪些栈帧。

编程题

  1. 写一个方法 int maxDepth(Node node) 求二叉树的深度,用调试器验证:单节点树返回 1,空树返回 0,并观察递归过程中调用栈的变化。
java
record Node(int value, Node left, Node right) {}

综合任务

  1. 找一个你以前用打印排查过的问题(或构造一个),改用调试解决:
    • 先用条件断点定位到问题发生的那一次迭代;
    • 再用求值(Evaluate Expression)检查当时的多个变量;
    • 写下"用打印需要几步、用调试需要几步"的对比。
分级提示

提示 1(方向):第 5 题的深度 = 左右子树深度的较大值 + 1,空树返回 0。

提示 2(关键点):第 5 题用 F7(步入 / Step Into) 进入递归调用,注意每层栈帧里的 node 值都不同。

提示 3(接近答案):第 6 题的对比重点不在步数,而在"调试能看到中间状态,打印只能看到你事先想到的状态"。

12. 参考答案 ​

基础题
  1. 错。步过(F8 / Step Over)把整个方法调用当成一步执行完,不进入内部;进入内部要用步入(F7 / Step Into)。
  2. 方法断点(在方法签名行下断点)。
  3. 因为打印语句本身是程序的一部分:它消耗时间、产生输出、可能触发同步,从而改变多线程的执行时序。最致命的场景就是并发问题的排查——加了打印,bug 就不复现了,这正是"海森堡 bug"的由来。
代码阅读题(第 4 题)

返回 10(4 + 3 + 2 + 1 + 0)。

Frames 面板从栈顶到栈底依次是:

text
mystery(0)      ← 最先返回
mystery(1)
mystery(2)
mystery(3)
mystery(4)
main

每按一次步入(F7 / Step Into)就压入一层,到 n == 0 后开始逐层弹出,每弹出一层就完成一次加法。

编程题(第 5 题)
java
package cn.kb.learn;

public class TreeDepth {
    record Node(int value, Node left, Node right) {
    }

    static int maxDepth(Node node) {
        if (node == null) {
            return 0;
        }
        return Math.max(maxDepth(node.left), maxDepth(node.right)) + 1;
    }

    public static void main(String[] args) {
        Node leaf = new Node(1, null, null);
        Node root = new Node(2, leaf, new Node(3, null, new Node(4, null, null)));

        System.out.println("空树深度 = " + maxDepth(null));
        System.out.println("单节点深度 = " + maxDepth(leaf));
        System.out.println("三节点链深度 = " + maxDepth(root));
    }
}

预期输出:

text
空树深度 = 0
单节点深度 = 1
三节点链深度 = 3

调试要点:maxDepth 会被调用多次(每个节点一次),在方法签名下方法断点会让你看清"同一层递归里调用了两次自己"。

综合任务(第 6 题)

参考答案的对比结构:

维度打印调试断点调试
定位第 873 次迭代加 if 判断再打印,改代码 + 重跑条件断点 i == 873,一次到位
查看当时的所有变量每个都加一行打印变量面板一屏看完
临时检查新表达式改代码、重编、重跑求值(Alt+F8)当场算
排查异常来源层层加打印异常断点直接停在抛出位置

核心差别:调试能看"所有状态",打印只能看"你事先想到的状态"。

13. 掌握标准 ​

  • [ ] 能说出编译期 / 加载期 / 运行期各自的典型错误;
  • [ ] 会用条件断点跳过大量无关迭代;
  • [ ] 能用 Frames 面板解释递归的执行顺序;
  • [ ] 会用求值(Evaluate Expression)在暂停时检查任意表达式。

14. 扩展资源 ​

15. 下一步学习 ​

《读懂 Java 错误信息》 会把报错信息拆解成可操作的线索;进入 S1 后,调试将成为你学习集合、多线程时的常规武器。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引