> 技术文档 > IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


断点调试


行断点


示例代码如下:

public static void main(String[] args) { basicBreakpointDemo();}// 1. 基本断点调试private static void basicBreakpointDemo() { System.out.println(\"=== 基本断点调试 ===\"); List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = 0; // 在这里设置普通断点 for (int num : numbers) { sum += num; System.out.println(\"Current sum: \" + sum); } System.out.println(\"Total sum: \" + sum);}

在如下位置打上断点:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


使用断点调试功能,运行 main 方法

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


我们打好断点后,以 debug 模式启动程序,程序就会停在断点上

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


当点击下面该按钮时,程序将跳过第一个断点之后的所有断点,并直接运行至结束:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


日志断点


如果我们要打一个日志断点,需要使用shift + 鼠标左键

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

打上详细断点后,IDEA 会跳出关于 断点详细配置 的面板;


Suspend


  • Suspend(挂起)选项决定了程序在到达断点时的行为:
    • 勾选 Suspend:程序会在到达断点时暂停,允许我们检查变量值、调用栈等信息。
    • 未勾选 Suspend:程序不会在断点处暂停,而是继续执行。即使是一个普通断点,如果未勾选 Suspend,程序也会直接跳过。

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


如果我们勾选了 Suspend 并点击 Done 按钮,那么该断点将从一个详细断点转变为普通的行断点。

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


接下来,我们在原位置重新设置一个详细断点,但不勾选 “Suspend”选项,然后点击“Done”,这样就成功添加了一个详细断点:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


重新以 Debug 的形式运行 main 方法,观察程序运行结果:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

此时我们发现:

  1. 程序不仅执行了 basicBreakpointDemo() 的逻辑
  2. 在输出结果中间(即详细断点所在的位置)插入了一句提示日志:“Breakpoint reached at DebugPractice.basicBreakpointDemo”

但是转念一想,断点断点,你为啥没给我断呢?

  • 黄色断点:在 IntelliJ IDEA 中,黄色断点通常表示这是一个 日志断点(Logpoint),而不是普通的断点。日志断点用于在代码执行到该位置时记录日志信息,但不会暂停程序的执行。
  • 普通断点``(红色断点):用于在代码执行到该位置时暂停程序,以便进行调试

并且,如果我们未勾选 Suspend:程序不会在断点处暂停,而是继续执行。即使是一个普通断点,如果未勾选 Suspend,程序也会直接跳过。

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


方法断点


在方法签名处打的断点,就是方法断点:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


使用 Debug,运行 main 方法,程序停在了方法断点下一行的位置:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


我们勾选方法断点的配置选项 Method exit

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


然后我们点击Reesume Program 按钮:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


这样,在按下 Resume Program 按钮后,程序不会立即执行完该断点所在的方法,而是停留在该方法的末尾位置:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


这一功能使我们能够在方法即将结束时,查看与该方法相关的所有变量的当前值:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


如果方法断点不勾选 Method exit,则按Reesume Program 按钮,调试的断点方法会立刻执行完毕:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


接下来,我们来看一下,方法断点对接口和对应实现类的调试作用:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

public class DebugPractice { public static void main(String[] args) { testGreeters(); } private static void testGreeters() { // 常规的对象创建方式 Greeter englishGreeter = new EnglishGreeter(); Greeter chineseGreeter = new ChineseGreeter(); // 常规的方法调用方式 englishGreeter.greet(); // 第一次调用 chineseGreeter.greet(); // 第二次调用 }}interface Greeter { void greet(); // 设置方法断点}class ChineseGreeter implements Greeter { @Override public void greet() { System.out.println(\"你好!\"); }}class EnglishGreeter implements Greeter { @Override public void greet() { System.out.println(\"Hello!\"); }}

只在接口方法打一个方法断点,其他地方都不打断点,接一下以 debug 的方式运行 main 方法

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


程序以 debug 的方式启动后,会自动跳转到该接口对应的实现类的方法中:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

因此,如果一个接口有多个实现类,并且我们想要知道接口中的方法是由哪一个实现类具体实现的,可以在接口对应的方法上设置一个方法断点

启动调试模式(Debug Mode)后,程序会在调用该接口方法时自动暂停,并跳转到具体的实现类方法中。通过这种方式,我们可以直观地确认是哪一个实现类被调用


异常断点


异常断点实例代码如下:

pulic class DebugPractice{ private static void exceptionBreakpointDemo() { Object o = null; o.toString(); // NullPointerException System.out.println(\"this line will never be print!\"); } public static void main(String[] args) { exceptionBreakpointDemo(); } }

如果在没有设置任何断点的情况下直接启动调试模式,程序将立即运行至结束,并且在控制台中输出空指针异常(NullPointerException)的日志。

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

接下来,我们来设置异常断点,设置异常断点,可以在程序出现异常前,让程序自动停在出现异常的地方,不继续往下运行;


在调试面板中,点击 view Breakpoint 按钮,会出现断点的控制面板:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


接下来,我们在断点控制面板中进行对应的设置:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


如果我们要监控某一种特定的断点,可以进行如下设置:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


再次 debug ,程序会自动停留在出现空指针异常的地方:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


字段断点


public static void main(String[] args) { fieldWatchDemo();}// 字段观察断点 | 读写监控private static void fieldWatchDemo() { User user = new User(\"John\", 30); user.setAge(31); // 在 User 类的 age 字段上设置字段观察断点 System.out.println(\"User updated: \" + user);}

在 User 类的 age 字段上设置字段观察断点,用于在调试时,监控 age 字段的生命周期:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


用 debug 模式启动程序:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


点击 Reesume Program 按钮执行下一步:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


再次点击 Reesume Program 按钮执行下一步:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

此时虽然还存在一个 System.out.println方法调用,但该调用实际上是通过 toString() 方法实现的,属于读操作。在点击“Resume Program”继续调试时,程序不会因这种读操作而暂停。


线程调试


Suspend


接下来,我们来看一下关于多线程的调试技巧,代码逻辑:

这段代码实现了一个多线程累加计算的Demo,主要逻辑如下:

  1. 创建两个线程:

    • thread1 计算1到100的累加和
    • thread2 计算1到100000的累加和
  2. 线程执行流程:

    • 每个线程启动后执行 run() 方法
    • 在 run() 中调用 add() 方法进行实际累加计算
    • 使用 BigInteger 处理大数运算
  3. 主线程控制:

    • 主线程通过 join() 等待两个子线程完成计算
    • 最后汇总两个线程的结果并打印
  4. 计算逻辑:

    • add() 方法实现从[1,n]的累加
    • 结果存储在 result 变量中

这是一个典型的多线程并行计算示例,展示了线程创建、启动、同步和结果汇总的基本模式。


以下是我们用于调试的多线程示例代码:

import java.math.BigInteger;public class ThreadDebugDemo { // 开启两个线程 public static void main(String[] args) { // 第一个线程计算 100! AddThread thread1 = new AddThread(100); // 第二个线程计算 10000! AddThread thread2 = new AddThread(100000); thread1.setName(\"thread1\"); thread2.setName(\"thread2\"); thread1.start(); thread2.start(); try { thread1.join(); // 线程 join , 以便主线程在 \"线程1\" 和 \"线程2\" 都返回结果之前不会进一步执行 thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } BigInteger result = thread1.getResult().add(thread2.getResult()); System.out.println(\"讲两个线程的计算结果相加等于: \" + result); } private static class AddThread extends Thread{ private BigInteger result = BigInteger.ONE; private long num; public AddThread(long num){ this.num = num; } @Override public void run() { System.out.println(Thread.currentThread().getName() + \" 开始计算: \" + num); add(num); System.out.println(Thread.currentThread().getName() + \"执行完成\"); } // 累加计算 public void add(long num){ BigInteger f = new BigInteger(\"1\"); for(int i = 2 ; i <= num; i++){ f = f.add(BigInteger.valueOf((i))); } result = f; } public BigInteger getResult(){ return result; } }}

我们先在thread1.join() 的位置打上一个断点:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

线程 join , 以便主线程在 “线程1” 和 “线程2” 都返回结果之前不会进一步执行


以 debug 模式运行对应 main 方法:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


我们点开多线程调用栈:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


我们先点击 Reesume Program 按钮执行完当前调试,右键刚刚打的断点,进入断点设置配置面板:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


我们勾选 Thread 模式,然后重新 debug ,我们会发现 call stack 调用栈只剩下 main 线程,Thread1、Thread2 则没有了:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


去掉刚刚的断点,我们再在 run() 方法打一个断点,并且将该断点的 Suspend 从 All 设置为 Thread

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


此时的 call stack 有 thread1 、thread2:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


并且,在切换栈帧的过程中,我们可以查看当前线程中字段的具体值。为此,必须将断点的 Suspend 选项从 All 设置为 Thread,当断点触发时,挂起范围从整个 JVM 进程的所有线程都会被挂起,到仅挂起触发断点的当前线程,其他线程继续运行

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


继续拓展,我们点击下面的按钮 Evaluate Expression,就是使用运行时表达式的解析功能:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


我们可以让Evaluate Expression打印当前系统运行的线程名:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

上面的功能,都是基于Suspend -> Thread(以线程为单位进行挂起)的设置实现的,哪个线程触发断点,哪个线程就被挂起,在 Evaluate Expression 中执行Thread.currentThread().getName(),从能准确获取线程


如果我们选择将断点挂起设置为 Suspend -> All,那么程序会暂停所有线程的执行,而不是仅暂停当前线程。在这种情况下,我们无法直接获取当前系统线程的上下文,因为所有线程都被暂停了。此时,线程的调度是随机的,哪个线程先获得上下文是不确定的。

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

后续即使通过 Evaluate Expression 功能执行 Thread.currentThread().getName(),无论我们如何切换栈帧,运行时表达式得到的结果都不会改变,始终是最初获得上下文的线程名称。


Condition


在断点设置面板中,我们不仅可以选择挂起线程的范围(Suspend),还可以自定义断点的调试条件(Condition),以下是我们接下来用于调试的示例代码:

import java.util.concurrent.TimeUnit;public class DebugAdvance { public static void main(String[] args) { condition(); } // 条件表达式 public static void condition(){ for (int i = 0; i < 10; i++) { System.out.println(i); } MyThread myThread = new MyThread(); Thread t1 = new Thread(myThread,\"thread1\"); Thread t2 = new Thread(myThread,\"thread2\"); Thread t3 = new Thread(myThread,\"thread3\"); t1.start(); t2.start(); t3.start(); } public static class MyThread implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + \"--进入\"); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }finally { System.out.println(Thread.currentThread().getName() + \"--离开\"); } } }}
  1. 主线程逻辑:
    • 先通过一个 for 循环打印数字 0 到 9。
    • 然后创建三个线程 t1t2t3,分别命名为 \"thread1\"\"thread2\"\"thread3\",并启动它们。
  2. 线程逻辑:
    • 每个线程在执行时,先打印 \"线程名称--进入\"
    • 然后暂停 100 毫秒(模拟耗时操作)。
    • 最后打印 \"线程名称--离开\"
  3. 运行结果:
    • 数字 0 到 9 会先被打印出来。
    • 三个线程并发执行,打印进入和离开的信息,具体顺序取决于线程调度。

接下来,我们在代码中如下位置设置断点。如果在该行代码中,i 是一个变量,那么在设置断点时,i 也会被识别为变量。因此,如果我们在断点的条件(Condition)中输入 i,它会被解释为当前行中的变量 i

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


此时右键点击断点,进入断点设置面板:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

注意:断点设置的 condition 并不会影响程序执行的最终结果,当满足 condition 条件时,断点才会被触发,程序才会停下来


利用好 condition 功能,就可以设置我们需要重点调试的地方,比如,如果我们在如下位置设置断点:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


此时,如果我们比较关系 thread1 线程的执行效果,就在断点的 condition 中进行如下设置,让该断点只要在执行的线程是 thread1 时,才会停顿

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


接下来,我们用 debug 模式运行 main 方法,我们发现

  • 当选择 Suspend -> Thread 时,调用栈中仅显示触发断点的线程(例如 thread1)的堆栈信息;
  • 当选择 Suspend -> All 时,所有线程都会暂停,但只有触发断点的线程(例如 thread1)能够获取当前上下文信息;
  • 因为我们断点对 condition 的设置,在调试过程中,只有当前线程名为 thread1,程序才会停下来,thread2 、thread3 则因为没有触发断点条件而被直接跳过;

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


打印堆栈信息


接下来,我们再来看看调试过程中,打印堆栈信息的调试功能,示例代码如下:

public static void main(String[] args) { printStackTrace();}private static void printStackTrace() { ArrayList list = new ArrayList(); list.add(1); list.add(2); list.add(3); list.add(4); System.out.println(list);}

比如我们想看某处代码的堆栈信息时,也可以先在该代码处打一个断点,并在断点中进行对应的设置:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


触发断点打印对应堆栈信息的配置如下:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


我们重新以 debug 模式运行 main 方法,执行程序过程如下:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


表达式分析


示例代码如下:

public static void main(String[] args) { evaluate();}// 表达式解析public static void evaluate(){ System.out.println(\"evaluate\"); User user = new User(\"kris\",33); List<Integer> list = Arrays.asList(1,2,3,4).stream() .map(x -> x*2).collect(Collectors.toList());}

为了更好演示表达式分析功能,我们先去掉上文在 User 类中打的字段断点;接下来,我们在如下位置打上断点,并重新以 debug 的模式运行 main 方法:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


以下是使用表达式分析的过程:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧在调试过程中,通过使用 Evaluate Expression 功能,我们可以对已经识别的变量进行以下操作:

  1. 计算表达式:对变量执行不同于原始代码的运算规则。
  2. 获取字段值:访问变量的字段属性。
  3. 修改字段值:更改变量的字段属性。

然而,需要注意的是,Evaluate Expression 仅在调试时评估表达式的结果,这些操作不会影响程序的实际运行或最终结果

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


避免操作资源


在日常开发过程中,我们经常会遇到一些意外情况:

  • 比如代码中出现了未被 try catch 捕获的异常;
  • 或者没有通过 if-else 语句进行判断;

导致错误数据被存储到数据库、Redis 中,甚至是一些重要的信息,被发送到消息队列(MQ)中。

 public static void main(String[] args) { saveRecourse(); } // 避免操作资源 | drop frames public static void saveRecourse(){ System.out.println(\"shit happens\"); // 模拟未被捕获的异常 System.out.println(\"save to db\"); System.out.println(\"save to redis\"); System.out.println(\"send message to mq for money payout\"); }

这种情况不仅会浪费我们大量的调试时间,还可能带来更严重的问题。
因为一旦这些错误数据进入数据库,我们可能需要花费更多的时间,去恢复或删除这些数据,才能使系统恢复正常运行,从而达到预期的效果。

为了避免这种情况的发生,我们需要学习如何在开发过程中,避免资源的错误操作和数据的异常存储


我们在如下位置打上断点,并且以 debug 模式启动程序

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


经过调试,我们已经知道程序在哪里出现了异常:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


那我们是否可以通过下列操作,来停止调试呢?

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

在调试过程中,即使我们通过断点定位到异常代码,但如果直接终止调试,后续的代码仍然会继续执行,包括可能将错误数据写入数据库、Redis 或发送到消息队列等操作。这会导致错误日志被记录,甚至污染数据存储


因此,仅仅通过终止调试是不够的。我们需要在定位到异常代码后,采取更有效的方式来避免后续对资源的操作:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


源码调试


示例代码

public static void main(String[] args) { sourceCode();}// 调试源码 | JDKpublic static void sourceCode(){ ArrayList arrayList = new ArrayList(); arrayList.add(1); arrayList.add(2); arrayList.add(3); System.out.println(arrayList.size()); LinkedList linkedList = new LinkedList(); linkedList.add(1); linkedList.add(2); linkedList.add(3); System.out.println(linkedList.size());}

以上代码是关于 ArrayList 和 LinkedList 添加数据 add 的代码,如果我们要查看 add() 的实现细节,可以进行如下操作:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


打上断点后,以 debug 模式运行 main 方法:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧

按照以上的调试技巧,我们就可以通过调试,看到调用的 API 中,内部的具体实现细节;


stream 调试


示例代码

public static void main(String[] args) { streamDebug();}// stream 调试public static void streamDebug(){ // stream chain Arrays.asList(1,2,3,45).stream() .filter(i -> i%2 == 0 || i % 3 ==0) .map(i -> i * i) .forEach(System.out::print);}

stream 调试步骤如下:

IntelliJ IDEA 调试技巧深度剖析:一网打尽开发中常用调试小技巧_idea调试日常技巧


在这里插入图片描述

在这里插入图片描述