Using IntelliJ to Debug + Understand your Code.

** The following is a quick debugging tutorial I wrote for the CS 180 java class at Purdue. I thought it might be useful to some so I decided to share it.**

IntelliJ has a great debugging tool that allows you to closely look at how your program executes step-by-step, as well as how methods and variables interact and change over time. Take a look at their tutorial for getting started with the debugger. This will give you the basic skill-set to debug your programs effectively as well as access to some additional documentation that might be helpful.

This includes, but is not limited to:

That’s the basics of debugging. The following is a more CS180-relative debugging guide based around the 3 main issues you’ll face when programming; compile time errors, runtime errors, and semantic errors.

Quick Note – Syntax vs Semantic: Syntax errors are usually caught at compile time, and have to do with violating rules of the java language which dictate how things should be written (closing all curly brackets for example). Semantic errors usually come from issues with the logic of your program and manifest themselves at runtime, either causing your program to crash or provide the wrong output (both cases which we’ll see).

The following material is supplementary to the above material, so it is a good idea to review the material above before diving in as we’ll cover how to interpret the stack trace and then understand the errors using IntelliJs debugger. Feel free to have IntelliJ open in parallel to walk through these debugging examples.

Compile time errors:

These are the errors that don’t even let your program get off the ground. A common example of this:

Error: (15, 13) java: /User/Directory/Program.java:15: cannot find symbol
symbol  : class Scanner
location: class Program

The compiler is giving you some great information here. Namely, there is something happening at line 15, 13 spaces over. In IntelliJ, press Command/ Ctrl + L and type in 15:13. This will take you to the location of the issue. What else does the stack trace tell us?

  • “Cannot find symbol” – Java can’t find something I’ve referenced in my code.
  • symbol: class Scanner” – That must be it; I am referring to a class “Scanner” at line 15:13 that java can’t find. Did I forget to import the Scanner class into my program? Yep.

Symbol errors aren’t just reserved for forgetting to import a Class. They can also come from uninitialized variables;

Error: (15, 13) java: /User/Directory/Program.java:15: cannot find symbol
symbol  : variable var
location: class Program 

 

Run time errors:

Run time errors occur during your program execution, the two most famous being “NullPointerException” and “IndexOutOfBoundsException.” For example, if I was trying to sum a jagged array like this:

int[][] matrix = {
        {1, 2, 3, 4},
        {5, 6},
};

// declare sum variable
int sum = 0;

// Compute sum
for(int i = 0; i < matrix.length; i++)
{
        for(int j = 0; j < matrix.length; j++)
            sum += matrix[i][j];
}

The compiler wouldn’t stop us from trying to run this code as it is syntactically correct. However, my program would crash at some point. Here is what the stack trace would tell me:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
	at Matrix.main(Matrix.java:24)
	...

Remember how we could find the line number of the error for compile time errors? Well we can do the same with run time errors as well. Something happened at line 24 to cause my Matrix.java program to crash. If eyeballing line 24 doesn’t immediately give away what the issue is, we can use IntelliJs debugging tools to find the issue.

 

There are 3 main debugging operations you’ll want to be familiar with:

  1. Set a breakpoint at a certain line in your code
  2. “Stepping” through your application
  3. Set different watch points for variables and expressions

 

Returning to our Matrix example, since something bad is happening at line 24, I am going to set a breakpoint there. When in debugging mode, this tells IntelliJ to stop just before this line is executed. To set a breakpoint, click to the right of the line number that you want to set the breakpoint at. IntelliJ will place a red circle there and highlight the line in dark red to indicate that a breakpoint has been set.

Now we are ready to debug. To do so, you can click the image of a bug in the top right that is next to the play button that you usually press to run your program.

Now you’ll have a footer in IntelliJ that looks something like this:

Take a few seconds to get familiar with this window. In frames, it tells us the method (main()) that we are currently in. Next to this, there is a list of variables and their values. As we “step” through the program execution, these values will update – allowing us to pinpoint the precise values for which our program breaks.

When we “step” through our program we are basically giving IntelliJ permission to continue running until it hits our breakpoint again. The line that is about to be executed is highlighted in blue.

So with that, let’s step through our program by clicking the “Step into” button:

After one click, you will notice that the value for sum has changed. Another click and you’ll see the value for ‘j’ has changed. We are inside the nested for loop, and each time we step we are letting IntelliJ update our sum variable. Keep clicking “Step” until one of your variables gives you an error:

What does this tell us? It tells us that matrix[i][j] produced an IndexOutOfBoundsException when i = 1 and j = 2. Does the index matrix[i][j] not exist? Let’s set some Watches and run through the execution again.

Let’s set watches for i (the row we are on), j (the column we are in), the matrix length, and the matrix width. To do so, click the plus sign and type the variable names you want to watch + enter.

Alright, now let’s click ‘Rerun Matrix’ in the top left of the Debugging footer to restart the execution.

Now, “step” through the execution again until you receive the IndexOutOfBoundsException error for one of your variables. Ignore the warning in Watches about not knowing what ‘j’ is, this is because you are not inside the inner for loop anymore so ‘j’ is out of scope.

Here is what we should have for Watches:

Remembering the Arrays lecture from week 5, we should notice a problem immediately. We have an array of i rows and j columns. Matrix.length gives us the number of rows in the matrix, but the row at matrix[i] only has 2 columns (matrix[i].length). Since the index of an array starts at 0, an array of length 2 only has indices 0 and 1. Yet, we are trying to access matrix[1][2] which does not exist. How did j get outside the number of columns? Looking at our inner for loop again, we see the problem.

for(int j = 0; j < matrix.length; j++)

Our j is bounded by the length of the matrix (number of rows), not the width of the specific row we are looking at, as given by matrix[i].length. Now, lets change that inner for loop to:

for(int j = 0; j < matrix[i].length; j++)

and Rerun our program through the debugger again. This time, we should get all the way through our loops and if we click “Console” at the top of the debugging footer, we will see a sum of 100.

For more practice, plus if you’re struggling with the “this” keyword, play around with the debugger in IntelliJ with the following code.

public class InstanceVar {
    public int key = 5;
    public InstanceVar(int key)
    {
        this.key = key;
    }
    public static void main(String[] args)
    {
        InstanceVar a = new InstanceVar(3);
        System.out.println(a.key);
    }
}

Set breakpoints at lines 9 and 10 (InstanceVar creation and the print statement). Add Watches for “this.key” and “key.”  Follow the blue “about to be executed” line and watch how this.key changes as the lines are executed. Take special note of this step just before we set this.key = key:

this.key already has a value of 5 derived from the instance variable declared before the constructor (line 2). When we pass a key value of 3 to the constructor, “step” forward and you will see that we are merely updating the key value for this instance of the class. Each additional InstanceVar object you create will each has its own value for key, depending on what you pass to the constructor.

Semantic Errors

The final frustrating case that can be aided by IntelliJs debugging tools is the case of semantic issues with your code. Your code compiles fine, doesn’t crash, yet the output isn’t what it should be. Two prominent examples of this are accidental integer division and incorrect String comparison. Luckily, IntelliJ can help.

Consider the nthRoot(double value, int root) method from Lab05 MathTools. Many students had an issue where their method would always produce 0. Often, it was because of this:

x_k_1 = (1/root) * (((root -1) * x_k) + ....;

With integer division, (1 / root) will always resolve to 0 as root is itself an Integer. By setting breakpoints and Watching certain variables, we can pin point this quicker and less painfully. For some practice, pull up your Lab05 code, change your x_k_1 formula to 1/ root, set a breakpoint where this formula is calculated, and step through the execution. To make things simpler, add a main method with a simple call to nThRoot like this:

public static void main(String[] args)
{
    System.out.println("3rd root " + nthRoot(125.0, 3));
}

The other big semantic issue that we often see is improper String comparison. In Java, Strings are objects not primitive types. Therefore, you can’t use logic comparisons on them ( == for example). All the Objects you declare and initialize have a reference in memory. To see an Objects reference, you can just print it. For example:

ClassName one = new ClassName();
ClassName two = new ClassName();
System.out.println(one + " " + two);
System.out.println(one == two);

You should see something like:

ClassName@457471e0 ClassName@7ecec0c5
false

When you use the == operator on two objects, you are comparing their reference values in memory NOT any value they hold. Change the 2nd print statement to

System.out.println(one == one);

This will return true, as you’re comparing the same reference address to itself. In java Strings are a special kind of Object that, although it behaves differently, still has a reference address behind it. While this is easy to see for arbitrary objects, people (myself included) often forget about this nuance with Strings and try to get away with something like:

String name = new String("Purdue");
String other = new String("Purdue");
if(name == other) 			// this is false
    System.out.println("Same name.");

Debugging this can be difficult if you’ve forgotten how Strings work, which is where IntelliJ comes in. If you set a breakpoint at the if-statement and a set a Watch for “name == other,” you’ll be able to quickly realize that there’s something wrong with your logic. In your variables panel, you can see the reference value that each String has in curly braces {String@600} , {String@601}. In your Watch panel you’ll see “name == other” resolve to false.

Extra Knowledge: There is a difference between String objects and String literals. Change your String declarations to this;

String name = "Purdue";
String other = "Purdue;

and Rerun your program with debugger. If you look at the variables panel you’ll see the reference value in curly braces will be the same, and your “name == other” Watches expression will be true. It’s complicated, but the difference is this;

  • using new tells java to explicitly create a new object, no questions asked.
  • The other type of String declaration is called a String literal. These Strings are placed in something called the String Constant Pool, from which the Java Virtual Machine (JVM) will try to optimize your programs memory usage by comparing and giving String literals that are the same value the same address.

 

For extra insight, watch how the JVM handles a situation where the actual value of a String literal changes during the execution. Run the following and set a breakpoint at the line “name = “IU”;”

String name = "Purdue";
String other = "Purdue";
name = "IU";
if(name == other)
    System.out.println("Both Purdue");

Before the line name = “IU”, JVM has given name and other the same reference address. However, during execution JVM realizes that the value for name is changing, so “name” will then get its own reference address.

Conclusion:
While the last example was a very advanced topic for this class, it’s a case where using breakpoints and setting Watches allows you to see intimately “under the hood” as your program executes. If you aren’t quite understanding how your program is actually working, using the debugger in IntelliJ is a great way to not only find bugs, but also deepen your understanding of the intricacies of Java. Additionally, it’s great for final exam preparation.

Advertisements