Tips & Tricks Tutorials

Debugger Basics in IntelliJ IDEA

If you’ve ever used System.out.println() to debug your code, this post is a must-read.

A debugger is a special tool that you can use to execute your code in a strictly controlled environment. It lets you review and analyze the inner state of your application and find and fix bugs that may be hidden deep within your code.

With a debugger, you also have the power to change and test the behavior of your code without modifying the source, and do a lot of other interesting things too. Let’s take our first steps into the world of debugging and see what this tool is capable of.

Executing code in debug mode

Starting a debugger for a console application is simple. Let’s use the following sample code to demonstrate:

package com.jetbrains;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class CoordinatesCopy {
   public static void main(String[] args) throws IOException {
       List lineCoordinates = createCoordinateList();
       outputValues(lineCoordinates);
       Point p = new Point(13, 30);
       removeValue(lineCoordinates, p);
       outputValues(lineCoordinates);
   }

   private static void outputValues(List lineCoordinates) {
       System.out.println("Output values...");
       for (Point p : lineCoordinates) {
           System.out.println(p);
       }
   }

   private static void removeValue(List lineCoordinates, Point p) {
       lineCoordinates.remove(p);
   }

   private static List createCoordinateList() {
       ArrayList list = new ArrayList<>();
       list.add(new Point(12, 20));
       list.add(new Point(13, 30));
       return list;
   }
}

There are a number of ways to start the debugger:

  1. You can click on the Run icon in the gutter area and select the Debug option.
  2. You can invoke context actions on the class or main method by using Alt+Enter and choose the Debug action.
  3. You can also start it from the Run menu, or by pressing Shift F9.

You can edit the Run configuration to add Virtual Machine options or to pass arguments to the program.

Pause, resume, restart, or stop the debugger

If your application seems to be unresponsive, you can pause the program to analyze where your code is stuck. Let’s modify the main method from the preceding section as follows:

    public static void main(String[] args) throws IOException {
        List lineCoordinates = createCoordinateList();
        outputValues(lineCoordinates);
        Point p = new Point(13, 30);
        int y = System.in.read();         // execution pauses here 
        removeValue(lineCoordinates, p);
        outputValues(lineCoordinates);
    }

Execute your application in debug mode. If you don’t input a value, your application would seem to become unresponsive. This might be easy to comprehend in this code, but not all codebases are so simple.

In the Debug window, click on Pause Program and the editor window will show the class and method your application is currently executing – or blocked on. In this example, you can see that the code is blocked for user input, showing the relevant class and method in the editor. You can also view the call stack. By clicking on the method calls in the call stack, you can view the corresponding class and method in the editor window.

You can resume program execution by clicking on Resume Program or by using the shortcut F9. To restart the program in debug mode, select Rerun. You can stop debugging your program at any time by using the Stop icon.

Notice that I didn’t set any breakpoints in this case.

Breakpoints

A breakpoint will stop the execution of your program, so that you can analyze the state of your code.

To set a breakpoint on a line of code, click in the gutter area or use the shortcut Ctrl+F8 ( Win/Linux) or ⌘ F8 (macOS). If you don’t want to stop execution every time it reaches a breakpoint, you can define a condition for the breakpoint. For example, let’s add a breakpoint in the method outputValues(), on the line of code that outputs the value of variable p and define a condition to stop code execution when the field y of reference variable p is equal to 30.

You can also drag-drop the breakpoint in the gutter and move it to another line of code. By default, clicking a breakpoint icon in the gutter will delete it (you can modify the default behavior in Settings | Preferences). But if you’ve defined conditions or other parameters for a breakpoint, you might prefer it to be disabled, rather than deleted, when you click on it. You can do this by right-clicking the breakpoint icon and selecting Disable. A tick indicates that there is information for this line of code, while a cross indicates that no information is available on this breakpoint.

To check how the breakpoint and its conditions work, execute the sample code included in this blog (in debug mode). You’ll see that this program will pause when the value of field y for variable p is 30.

There’s much more to breakpoints. You can right-click on the breakpoint icon in the gutter and click on More. In the dialog that opens, you can modify a breakpoint so that it doesn’t suspend the program execution and instead logs an expression when it is reached. Let’s log the value of the x and y fields of the Point class and rerun our code. Now the code execution doesn’t stop at the breakpoint – instead it logs the expression we defined to the console.

Debug Window

The debug window displays important information when your application suspends execution on a breakpoint, like frames, threads, console window, step action icons, variables pane, and much more:

If you close the Debug Window by mistake, you can always reopen it using the shortcut Alt+5 ( Win/Linux) or ⌘ 5 (macOS). As usual, there are multiple ways to access the Debug Window. You can also access it using the ‘Search everywhere’ feature (shortcut: Shift+Shift), using Find Action (shortcut: Ctrl+Shift+A for Win/Linux and ⇧ ⌘ A for macOS), and by searching for ‘Debug’.

You can also view the debug window after your application finishes executing in debug mode. You can use the debug window not only to navigate through your code in debug mode, but you can also use it to view all the breakpoints in your program, mute them, and further modify what they do.

Before we move forward with the step actions that are used to debug an application, let’s understand the sample application used in this example.

Overview of the sample application

The code used in this blog is simple. The method createCoOrdinateList() creates two instances of the Point class and adds them to an ArrayList. The Point class has two fields, x and y, and getter and setter methods. The outputValues() method outputs the passed list items to the console. The next line of code creates a Point instance and the removeValue() method tries to remove it from the lineCoordinates list.

When you execute this code, you’ll see in the output that even though a Point with x and y values 13 and 30 were added to the list, when another instance with identical values was created to remove it, it was not successful. Let’s debug the code.

To debug your code, you’ll need to know the various step actions you can use to move through your code to find the bugs.

Step Actions

There are different ways to navigate your code in debug mode. For example, you might prefer to execute a line of code without bothering about the details of the methods being called. Or you might prefer to see which lines of code execute when you call another method from your application, libraries, or APIs. You can do this through the various step actions.

Set a breakpoint before you start the application in the debug mode. The various step actions are:

  • Step Over (F8) lets you execute a line of code and move on to the next line. As you step through the code, you’ll see the value of the variables next to the code in the editor window. These values are also visible in the Variables pane in the Debug window.
  • Step Into (F7) will take you to the first line of code in a method defined in the same class, or another class in the application.
  • Force Step Into lets you debug methods defined in the APIs or libraries. If the source code of the API or library is not available, IntelliJ IDEA decompiles and debugs it for you.
  • With Step Out, you can skip stepping through code in a method line by line and return to the calling method. The called method executes, but without stepping through each line of code in it.
  • Drop Frame lets you move back through code execution by dropping a method call.

Let’s use all the preceding actions to debug the Coordinates class. We’ll start by stepping over the lines of code in the main() method, stepping into the removeValues() method, and force-stepping into the remove() method of the ArrayList class and the equals() method to check how the values of the lineCoordinates list are being compared with the value of reference variable p, so that a matching value can be removed from the list. If you move forward to a line of code while debugging, you can use Drop Frame to drop a frame from the call stack and return to the calling method.

In the sample application, we discovered that the ‘bug’ is caused by the way the equals() method compares values. It returns true only if the references match, not if their corresponding field values match.

Let’s fix this bug by overriding the equals() method in the Point class.

Now, let’s rerun the code and check whether it is working as expected. Start the application and view the result.

Everything looks good now. We managed to find a bug and fix it too!

If you’re switching through classes and reading through code and miss where the code was executing, you can click on Show Execution Point. To skip executing code line by line, you can move forward to a line and click on Run to Cursor.

Variables Pane

The inline debugger is very helpful since it shows the value of the variables in the editor as you step through the code. However, the Variables pane shows a lot more details.

In this example, since we didn’t override the toString() method for the Point class, the editor window shows the class name and the debugger object id, which doesn’t seem to be very helpful. The debugger pane shows all fields of variables, including private fields.

Clicking on stacks will show us the variables that are relevant to that stack.

You can right-click on a variable and select Jump To Source (F4) to view where it was declared to understand your code better. By selecting the option Jump To Type Source (Shift+F4), you can also view the definition of non-primitive variables.

In a call stack, you might want to evaluate an expression to verify your assumptions. For example, I can evaluate the value of the this variable, or other valid expressions, like this is double equal to an instance of the Point class, or this is .equals() to an instance of the Point class.

You can create a variable whose value is accessible in all the call stacks by adding a new watch. Say, System.getProperty, and use the name of your OS.

You can create watches to view the value of certain variables in all the call stacks. There are multiple ways to do that. You can right-click in the code in the editor and select Add to watches. In the Variable pane, you can also click on the variable and drag and drop it to the Watches pane.

The values of these variables might not be available in all the call stacks. It really depends on the scope of the variable.

Modify code behavior

Did you know you can change the behavior of your code without changing its source? And this applies to the code defined by another API or framework too.

In this code execution, the x and y fields of Point instances being compared are equal, and this equals() method is about to return true. We can change the value of a variable by right-clicking it in the variable pane and selecting Set Value…. When we do this the behavior of the code changes. With the modified value, the equals() method returns false and this value won’t be removed from the ArrayList.

Summary

The debugger is a powerful and versatile tool that executes programs in a controlled environment. With a debugger, you can see the inner state of an application, find bugs, understand code, and do many other things.

Stay tuned for the next blog on the debugger in which we’ll cover advanced features like Remote Debugging, Breakpoint types, running tests until they fail, and much more.

image description