In this topic, you will continue learning practical ways to debug your code. We will focus on how to inspect and control the execution of methods in classes. We will explore the following aspects:
Setting breakpoints inside a method.
How to trace method calls between two classes.
Stepping through method calls.
Distinguishing between line and method breakpoints.
How to reset a method’s frame.
By mastering these techniques, you will gain greater control over the debugging process and identify issues more efficiently. It helps you analyze program behavior and troubleshoot issues you face in your code.
Setting breakpoints in methods
One of the first steps in debugging a method is to set a breakpoint at a specific line of code within that method. A breakpoint causes the running program to pause its execution when it reaches that line, allowing you to investigate what is happening at that exact moment. Sometimes setting breakpoints in different parts of a method can help you better examine the flow of data and setting it at the method's entry point will confirm whether the method is called at all.
Below is a simple example:
public class Greeter {
public static void main(String[] args) {
greetUser("Alice");
}
public static void greetUser(String name) {
String greeting = "Hello, " + name;
System.out.println(greeting);
}
}
1. Click the gutter at the first line inside the greetUser() method:
2. Start your program in Debug mode. The execution will pause at the breakpoint, allowing you to inspect or modify variables:
Many developers use hotkeys to speed up the debugging process. For example, instead of running the debug mode by clicking buttons in IntelliJ IDEA, you can use hotkeys. To start the debug mode, press Control+D on Mac OS and Shift+F9 on Windows.
On the left side, you can view the current call stack. In our example, you see that the greetUser() method was called from the main() method:
With a deep call stack, this tool helps you visualise the execution chain and track method values one by one.
Stepping through method calls
When your debugger is paused at a breakpoint, you have several options to control the flow of your program:
Step Over (F8): Execute the current line and move to the next one without diving into any function calls on that line.
Step Into (F7): Move execution into the method call on the current line, allowing you to debug inside it.
Step Out (Shift + F8) (sometimes called Step Return): Continue execution until the current method finishes, returning you to its caller.
Consider the following snippet in StepDemo.java:
public class StepDemo {
public static void main(String[] args) {
int result = calculateSum(5, 10);
System.out.println("The sum is: " + result);
}
public static int calculateSum(int a, int b) {
System.out.println("Calculating the sum...");
return a + b;
}
}
Set a breakpoint on the first line of the main() method:
Now let's see how each debugger option works.
Step Over keeps you in the
main()method, skipping the internal details ofcalculateSum(). The debugger moves you to the next line of themain()method immediately:
Step Into jumps inside
calculateSum(), pausing execution on the first line with printing "Calculating the sum...". InsidecalculateSum(), you can continue debugging using Step Over for the next line, which adds the two numbers:
Step Out finishes the execution and jumps outside
calculateSum()to the line where it was called. Use Step Out insidecalculateSum(), and it returns you to the point where you used Step Into in themain()method:
Using these stepping options, you can easily track which methods are called, when they are called, and why your program follows a certain execution path.
Setting breakpoints across classes
Often, you have methods in one class that call methods in another class. When debugging such scenarios, you need to set breakpoints strategically so you can pause execution and observe each step in detail.
Here is an example with two classes, Caller and Responder, that are placed in different files:
class Caller {
public static void main(String[] args) {
startConversation();
}
private static void startConversation() {
String reply = Responder.getResponse("Hello from the Caller!");
System.out.println(reply);
}
}
class Responder {
public static String getResponse(String greeting) {
return greeting + "\n...This is the Responder replying back.";
}
}
1. Let's set a line breakpoint inside startConversation(). Click the gutter on the first line in method in Caller class:
2. Run your application in Debug mode. The execution will pause before calling the getResponse() method:
3. Click the Step Into button (or F7) to jump into the getResponse() method in the Responder class, which is located in another file:
You see how the debugger moves you to the next call, even when classes are in separate files. By hovering over the variables with your mouse, you can monitor their values and confirm that the Responder returns the expected string.
Line breakpoints vs. method breakpoints
You can use different types of breakpoints depending on your debugging needs:
Line Breakpoints: Placed on specific lines of code. The debugger halts execution when it reaches that exact line. We have done that in each previous code example.
Method Breakpoints: Placed on the method definition rather than a line of code inside the method. This type of breakpoint is triggered whenever the method is entered or exited.
For instance, you could set a method breakpoint right on the getResponse() method in Responder.java, and the debugger will pause as soon as getResponse is invoked:
Method breakpoints are especially useful for tracking all calls to a method, particularly when multiple classes invoke it.
Resetting the frame
IntelliJ IDEA has a feature called Resetting the Frame. This allows you to rewind execution to the beginning of the current method or stack frame. It can be useful in some cases:
If you step over (or into) too many lines and want to recheck the logic in the same method.
When you suspect a bug occurs at the start of a method and you want to recheck the initial values.
If you want to experiment with variable values for the same method. For example, you can reset the frame, adjust variables with the debugger's Evaluate Expression tool, then rerun that section of code.
Below is an example that shows how you might debug and then rewind the method to recheck the logic without restarting the entire program.
public class PurchaseOrder {
public static void main(String[] args) {
double totalCost = processOrder(9.99, 5);
System.out.println("Total cost: " + totalCost);
}
public static double processOrder(double price, int quantity) {
double totalPriceBeforeTaxes = price * quantity;
updateInventoryInfo(quantity);
double totalPriceAfterTax = Math.round(totalPriceBeforeTaxes * 1.07);
updateTaxInfo(totalPriceAfterTax);
return totalPriceAfterTax;
}
private static void updateInventoryInfo(int quantity) {
System.out.println("Inventory is updated. Sold quantity: " + quantity);
}
private static void updateTaxInfo(double afterTaxPrice) {
System.out.println("Tax info is updated. Price after tax: " + afterTaxPrice);
}
}
1. Set a line breakpoint on the first line in the main() method:
2. Step Into processOrder() to see how it changes the variables totalPriceBeforeTaxes and totalPriceAfterTax:
3. Suppose you realize you need to verify processOrder() one more time carefully without restarting the entire program. Use the Reset Frame feature in the Debug section. Click a round arrow or press button Delete:
4. After resetting that frame, you can Step Into the same method from the beginning.
Now let's try to set a new value to the quantity variable at runtime and check the result. To do that, you should:
1. Set a line breakpoint on the first line in the main() method and Step Into processOrder().
2. Open the Evaluate pop-up by right-clicking inside the code editor window. Or press Option+F8 on Mac OS and Alt+F8 on Windows:
3. Assign the value 10 to the quantity variable, press Evaluate, and close the window:
4. The debugger now highlights the newly assigned variable value. We can go through processOrder() and check the return value with the reassigned quantity variable:
However, resetting the frame is not a silver bullet and has limitations. You only revert the local call stack, but not the entire application's external or global state. For example, if your code had already modified files, printed to the console or changed shared static fields.
Conclusion
Debugging methods requires a structured approach to examining code flow, inspecting variables, and identifying issues. Here are the key takeaways:
Set breakpoints inside methods or across classes to pause the program flow and inspect the current state.
Use stepping controls to navigate method calls with precision.
Step Over to go to the next execution line.
Step Into to jump inside the method.
Step Out to jump outside the method to the place where the method was called.
Use a line breakpoint to pause the program at an certain line and a method breakpoint to pause the program each time a method is called.
Use Reset Frame to re-check the logic without restarting the entire program.
By applying these techniques into your daily programming and using hotkeys for repetitive debugging tasks, you will significantly reduce the time spent searching for bugs and gain deeper insight into how your code executes.