By the end of the previous lesson, BiblioTechApp was already going through a queue of returns, filtering with continue, stopping with break and maintaining half a dozen accumulators. And with that complexity comes, inevitably, the moment when the program compiles, runs without complaining and produces a number that is not the right one. That is where what you can solve by rereading the code ends and debugging begins. Debugging is not a trick or an innate talent: it is a method, and it is probably the skill that most separates a programmer who moves forward from one who gets stuck. For somebody learning on their own it is even more critical, because there is nobody next to you to ask "why does it give me 0?". This lesson teaches you to see your program's internal state: first with traces, then with the IDE debugger, and always following a procedure that turns blind searching into an orderly investigation.
Contents
- Why reading the code is not enough
- The three kinds of error and which one gets debugged
- Debugging with traces: strategic
System.out.println - What to print in a trace and in what format
- The entry/exit trace pattern for a block
- Why traces must disappear (and what replaces them)
- The IDE debugger: concepts and start-up
- Breakpoints and stepping commands
- Variables, expressions and
watch - Conditional breakpoints
- Reading a stack trace
- The systematic debugging method
- A practical case: the fine summary that comes out wrong
- Common Mistakes and Tips
- Exercises
- Why reading the code is not enough
When a program does not do what you expect, the temptation is to reread it. The problem is that you reread what you thought you had written, not what you actually wrote. Your brain fills in what is missing, skips over the <= that should have been < and does not see the declaration that is inside the loop instead of outside.
Debugging works the other way round: instead of reasoning about what the code should do, you observe what it does. You look at the real value of every variable at every moment and compare it with the expected value. As soon as you find the first point where the real and the expected diverge, you have located the bug: it is between that point and the previous one that was still correct.
That difference between deducing and observing is the whole lesson.
- The three kinds of error and which one gets debugged
| Kind | When it appears | Who reports it | Example | How it is solved |
|---|---|---|---|---|
| Compilation | When compiling | javac or the IDE, with a line and a message |
A missing ;, incompatible types |
Read the message (lesson 01-03) |
| Runtime | When running | The JVM stops the program with a stack trace | NumberFormatException when converting "twenty" |
Handled in module 6; here we only read them |
| Logical | It never "appears" | Nobody. The program works and gives an incorrect result | The fine total comes out as €0.00 | Debugging |
Logical errors are the dangerous ones: they break nothing, they simply lie. A receipt with a badly calculated fine prints just as neatly as a correct one. And they are exactly what this lesson targets.
- Debugging with traces: strategic
System.out.println
System.out.printlnThe oldest technique and still one of the most used: printing the program's state at chosen points. It is called printf debugging or trace debugging.
Its advantages are real: it needs no tools, it works in any environment, it leaves a record you can read in one pass and it works even where the debugger cannot reach (concurrent code, remote processes). Its drawback too: it forces you to modify the code, recompile and run again on every attempt.
A badly placed trace is worth nothing:
When you run it you will see here, entering and a bare number, without knowing which of the five variables it is or which iteration you are in. A useful trace says where it is, which variable it shows and what it is worth:
System.out.println("[loop] n=" + n + " daysLate=" + daysLate
+ " fine=" + fine + " total=" + outstandingTotal);Output:
[loop] n=1 daysLate=37 fine=9.25 total=9.25 [loop] n=2 daysLate=74 fine=18.5 total=27.75 [loop] n=3 daysLate=111 fine=20.0 total=47.75
At a glance you check the progression and spot the exact point where the number stops being what you expected.
- What to print in a trace and in what format
Practical rules, ordered by usefulness:
- Always print the variable's name next to its value.
fine=9.25is information;9.25is noise. - Mark the location with a label in brackets.
[validation],[loop],[summary]. When you have fifteen trace lines, that label is what tells you where each one came from. - Include the loop's control variable. Without
n=3you do not know in which iteration the problem happens. - Use
printfwhen the format matters.doubles with many decimals are unreadable:System.out.printf("[loop] n=%d fine=%.2f%n", n, fine); - Use
System.errfor the exceptional. It shows in red in most IDEs and goes through a different channel, so you can separate the normal trace from the anomalous one. - Trace the
booleans and the conditions too. It is common to discover that the condition you thought was true is false:
boolean paid = (n % 2 == 0);
System.out.println("[filter] n=" + n + " paid=" + paid
+ " -> " + (paid ? "SKIPPED" : "PROCESSED"));- Delimit the loop from outside. Printing the state just before entering and just after leaving tells you whether the problem is in the loop or in what comes before or after:
System.out.println("[before] total=" + outstandingTotal + " reviewed=" + reviewed);
for (...) { ... }
System.out.println("[after] total=" + outstandingTotal + " reviewed=" + reviewed);
- The entry/exit trace pattern for a block
When a block of code transforms some input data into some output data, the most profitable pattern is to print both ends:
// INPUT of the calculation block
System.out.printf("[calc:IN ] elapsedDays=%d LOAN_DAYS=%d%n",
elapsedDays, LOAN_DAYS);
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
daysLate = 0;
}
double fine = daysLate * DAILY_RATE;
if (fine > MAX_FINE) {
fine = MAX_FINE;
}
// OUTPUT of the calculation block
System.out.printf("[calc:OUT] daysLate=%d fine=%.2f%n", daysLate, fine);With that you can apply bisection: if the input is correct and the output is not, the bug is inside that block. If the input already arrives wrong, the bug is earlier and there is no point looking here. Each pair of traces splits the program in two and discards one half. It is the same principle as binary search, and it is the fastest way to corner an error in a long program.
When you reach module 3 and start writing methods, this pattern will become the standard: one trace on entry with the parameters received and another on exit with the value returned.
- Why traces must disappear (and what replaces them)
Traces are scaffolding, not architecture. Leaving them in the code has real consequences:
- They pollute the output. The BiblioTech user must not see
[loop] n=3 fine=20.0in the middle of their receipt. - They cost performance. Console writing is extremely slow compared to any calculation; thousands of
printlns in a loop can multiply the running time by ten. - They leak information. In a real system, printing user data or credentials to the console is a security problem.
- They cannot be switched off. A
System.out.printlnis either there or not; there is no middle ground.
So, as soon as you find the bug, delete the trace. And for the information you do want to keep permanently, there is logging: a system with levels (DEBUG, INFO, WARN, ERROR) that can be enabled or disabled by configuration without touching the code, with a timestamp, an origin and a configurable destination (console, file, central server). That is studied in lesson 06-07 (Error Handling Strategies and Logging). For now, the rule is: trace to investigate, delete when done.
A transitional trick in the meantime, applicable with what you already know:
final boolean DEBUG = true; // set it to false to silence every trace
if (DEBUG) {
System.out.printf("[loop] n=%d fine=%.2f%n", n, fine);
}A single constant controls all the traces. It is a rudimentary version of what a logging system does, and it works perfectly well for a console program of this size.
- The IDE debugger: concepts and start-up
A debugger is a tool that runs your program under control: it can pause it on any line, show you the value of every variable at that instant and advance instruction by instruction. It does not modify the code and does not require recompiling to change what you observe. It is by far the most efficient way of investigating a logical error.
Every Java IDE (IntelliJ IDEA, Eclipse, VS Code with the Extension Pack for Java, NetBeans) includes one, and they all work with the same concepts and practically the same shortcuts, because underneath they use the same JVM technology.
To start in debug mode:
- IntelliJ IDEA: the bug icon button next to the run one, or
Shift + F9. - Eclipse: the
Run > Debug As > Java Applicationmenu, orF11. - VS Code: the
Run and Debugtab, orF5.
The difference from a normal run is that, if there is an active breakpoint, the program will stop when it reaches it and hand control back to you.
- Breakpoints and stepping commands
A breakpoint is a mark on a specific line telling the debugger "stop here". You place it by clicking in the editor's left margin, next to the line number; a red circle appears. Clicking again removes it.
Where to put them, in order of usefulness:
- On the first line of a suspect loop's body.
- On the line where the variable that comes out wrong is assigned.
- Just before and just after the block you want to examine.
- On the branch of an
ifyou believe is never executed (if the program stops there, your belief was false; extremely valuable information).
Once stopped, you advance with these commands:
| Command | IntelliJ | Eclipse | What it does |
|---|---|---|---|
| Step Over | F8 |
F6 |
Runs the whole current line and stops on the next one. If there is a method call, it runs it entirely without entering |
| Step Into | F7 |
F5 |
The same, but it enters the called method to debug it line by line |
| Step Out | Shift + F8 |
F7 |
Finishes running the current method and returns to its caller |
| Resume | F9 |
F8 |
Continues normal execution until the next breakpoint (or the end) |
| Run to Cursor | Alt + F9 |
Ctrl + R |
Runs up to the line where the cursor is, without placing a breakpoint |
| Stop | Ctrl + F2 |
— | Kills the process |
In this module all your code is in main, so Step Over is the command you will use 95 % of the time: pressing it repeatedly walks through the program line by line. Step Into will make sense in module 3, when you have methods of your own. A tip about Step Into: if you press it on a line with System.out.println, you will end up inside the Java standard library's source code, which is not what you wanted; use Step Over for calls that are not yours.
A typical debugging session's flow:
flowchart TD
A["Place a breakpoint on the suspect line"] --> B["Start in debug mode"]
B --> C["The program stops at the breakpoint"]
C --> D["Read the Variables window"]
D --> E{"Are the values as expected?"}
E -- "yes" --> F["Step Over: advance one line"]
F --> D
E -- "no" --> G["Bug located between the last<br/>correct check and this one"]
G --> H["Fix it and run again"]
- Variables, expressions and
watch
watchWhen the program is stopped, the IDE shows a Variables panel with everything that exists at that point: the parameters, the local variables and their current values. It is a photograph of your program's state.
At a stop inside BiblioTech's review loop you would see something like this:
| Variable | Value |
|---|---|
args |
String[0] |
DAILY_RATE |
0.25 |
MAX_FINE |
20.0 |
n |
3 |
daysLate |
111 |
paid |
false |
fine |
27.75 |
outstandingTotal |
9.25 |
reviewed |
1 |
Just by reading that table you already know which iteration you are in and whether the accumulators hold what they should.
Two complementary tools:
Expression evaluation. It lets you type any Java expression and see its result in the current context, without modifying the code. In IntelliJ it is Alt + F8 (Evaluate Expression); in Eclipse, the Expressions view or Ctrl + Shift + I over a selection. It is extremely useful for checking a condition before it is evaluated:
daysLate * DAILY_RATE -> 27.75 fine >= MAX_FINE -> true daysLate <= MINOR_THRESHOLD -> false n % 2 == 0 -> false outstandingTotal + fine -> 29.25
Asking the debugger "what is this condition worth right now?" solves many bugs in thirty seconds.
Watch (watched expressions). An expression added to the watches list is re-evaluated automatically at every stop. It is useful for keeping an eye on an accumulator across all the iterations without hunting for it in the variables list. Add outstandingTotal and reviewed as watches and you will see them evolve pass by pass.
- Conditional breakpoints
A 500-iteration loop with a normal breakpoint forces you to press Resume 500 times. A conditional breakpoint only stops the program when a condition you write is satisfied.
You configure it by right-clicking the breakpoint's red circle; a Condition field appears where you write a boolean expression that is valid at that point:
With that condition, the program will only stop on the iterations where the delay exceeds 30 days. Other conditions commonly used in BiblioTech:
| Condition | What it is for |
|---|---|
n == 7 |
Going straight to the problematic iteration |
daysLate > 30 |
Stopping only on the severe cases |
fine >= MAX_FINE |
Seeing exactly when the cap is applied |
outstandingTotal > 50.0 |
Discovering at what point the accumulated amount shoots up |
title.equals("Refactoring") |
Stopping only on one specific book |
!paid && daysLate == 0 |
Catching an odd combination you suspect |
It is the difference between reviewing 500 stops and reviewing 3. Together with expression evaluation, it is the debugger feature that saves the most time.
- Reading a stack trace
When a runtime error occurs, the JVM stops the program and prints a stack trace to System.err: the list of calls that were active at that moment. You do not yet know how to handle errors —that is module 6— but you do need to know how to read them, because you are going to see them from now on.
Exception in thread "main" java.lang.NumberFormatException: For input string: "twenty" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:665) at java.base/java.lang.Integer.parseInt(Integer.java:781) at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:23)
You read it like this, and the order matters:
- Line 1: the thread (
main), the type of error (java.lang.NumberFormatException) and the message (For input string: "twenty"). This line usually already tells you what happened: something tried to convert into a number a piece of text that was not one. - The following lines: the call stack, from most recent to oldest. The first is where it blew up; the last is where everything started.
- The line that matters to you is the first one mentioning YOUR code:
com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:23). There you have the class, the method and the line number. The ones above are insidejava.base, the standard library, and are almost never the problem.
Practical rule: read the first line to know what happened and look for the first line with your package to know where. In IDEs, those lines are links: one click takes you to the code.
The three runtime errors you will see most in this module:
| Error | Typical cause in BiblioTech |
|---|---|
NumberFormatException |
Integer.parseInt("twenty") or an empty string |
NullPointerException |
Calling a method on a String variable that is null |
ArithmeticException: / by zero |
Integer division by zero (it does not happen with double: it gives Infinity) |
- The systematic debugging method
Having tools is not enough; you need a procedure. This one always works and avoids three hours of changing things at random:
1. Reproduce. Find input data with which the failure happens every time. A bug you cannot reproduce you can neither fix nor verify. Note down exactly which inputs you use: in BiblioTech, for example, "Marta Ruiz / 3 books / 27, 10, 120 days".
2. Define what is expected. Write down the correct result before looking at the code. "The total should be €23.00". Without that reference you cannot know when you have finished. Work it out by hand if necessary.
3. Isolate by bisection. Split the program in half with a trace or a breakpoint and check whether the state at that point is correct. If it is, the bug is after; if it is not, it is before. Repeat on the surviving half. Ten steps corner a bug in a thousand lines.
4. Formulate a concrete hypothesis. Not "something is failing in the loop", but "I suspect totalFines is being reset on every iteration because it is declared inside the for". A hypothesis must be testable.
5. Verify it. Place a breakpoint or a trace that confirms or rules out that exact hypothesis. If it rules it out, go back to step 4 with another one. Never change the code to "see what happens" before you have verified: it is the most effective way of introducing a second bug on top of the first.
6. Fix. One single change, the minimum one, attacking the cause and not the symptom. If the total comes out wrong, the solution is not to add a constant to it at the end.
7. Check. Run it again with the data from step 1 and verify the result from step 2. And then try other data sets, especially the boundaries: zero books, zero days, values right on the exact edge of each condition. A fix that repairs one case and breaks two others is worse than the original bug.
- A practical case: the fine summary that comes out wrong
Let's take a real bug, the kind that will happen to you next week. The Nexus Software librarian processes a batch of three returns and the total comes out wrong.
The code (with the bug):
package com.nexussoftware.bibliotech;
public class BiblioTechApp {
public static void main(String[] args) {
final int LOAN_DAYS = 15;
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
String employee = "Marta Ruiz";
int returns = 3;
int lateBooks = 0;
System.out.println("Processing returns for " + employee);
for (int n = 1; n <= returns; n++) {
double totalFines = 0.0; // <-- line 22
int elapsedDays = 15 + (n * 37) % 130;
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate < 0) {
daysLate = 0;
}
double fine = daysLate * DAILY_RATE;
if (fine > MAX_FINE) {
fine = MAX_FINE;
}
if (daysLate > 0) {
lateBooks++;
}
totalFines += fine;
System.out.printf(" Book %d: %d days late, %.2f EUR%n",
n, daysLate, fine);
if (n == returns) {
System.out.printf("TOTAL: %.2f EUR (%d late books)%n",
totalFines, lateBooks);
}
}
}
}Step 1: reproduce. It is run and always gives the same thing:
Processing returns for Marta Ruiz Book 1: 37 days late, 9.25 EUR Book 2: 74 days late, 18.50 EUR Book 3: 111 days late, 20.00 EUR TOTAL: 20.00 EUR (3 late books)
Step 2: define what is expected. By hand: 9.25 + 18.50 + 20.00 = €47.75. The program says €20.00. The individual amounts are correct; it is the total that fails. And it is striking that €20.00 is exactly the last book's fine.
Step 3: isolate. The total is calculated inside the loop, so the loop is the suspect. We place a breakpoint on the totalFines += fine; line and start in debug mode. The variables window at each stop:
| Stop | n |
fine |
totalFines before the addition |
totalFines expected before |
|---|---|---|---|---|
| 1st | 1 | 9.25 | 0.0 | 0.0 ✔ |
| 2nd | 2 | 18.5 | 0.0 | 9.25 ✘ |
| 3rd | 3 | 20.0 | 0.0 | 27.75 ✘ |
At the second stop, totalFines is 0.0 when it should be 9.25. That is the exact point where the real and the expected diverge.
Step 4: hypothesis. If the accumulated value is lost between one iteration and the next, the variable is being recreated. And indeed: double totalFines = 0.0; is inside the loop (line 22). On every pass a new variable is declared, initialised to zero, and the previous one disappears at the closing brace.
Step 5: verify. A breakpoint is placed on line 22 itself, with the condition n > 1. The program stops there on the second iteration: the line does run on every pass. Hypothesis confirmed. (Without a conditional breakpoint you would have to stop on every iteration; with it, you go straight to the interesting one.)
Step 6: fix. The minimal change is to move the declaration out of the loop, next to the other accumulator:
int lateBooks = 0;
double totalFines = 0.0; // <-- moved OUTSIDE the loop
for (int n = 1; n <= returns; n++) {
// ... it is no longer declared here ...
}Step 7: check.
Processing returns for Marta Ruiz Book 1: 37 days late, 9.25 EUR Book 2: 74 days late, 18.50 EUR Book 3: 111 days late, 20.00 EUR TOTAL: 47.75 EUR (3 late books)
It matches the hand calculation. And now the boundaries get tested: with returns = 0 the loop never runs and no total is printed at all, because the if (n == returns) is inside the loop. That is a second bug, subtler, that the first test did not reveal. The proper fix is to take the summary out of the loop, which is also where it belongs conceptually:
for (int n = 1; n <= returns; n++) {
// ... calculation and printing for each book ...
}
// Summary OUTSIDE the loop: it always prints, even with zero returns.
System.out.printf("TOTAL: %.2f EUR (%d late books)%n",
totalFines, lateBooks);This case brings together the two most frequent loop errors: the accumulator declared inside and the summary computed inside. And it demonstrates step 7: if you had stopped at "it works now", the empty-case bug would still be there.
A variant to practise on: the off-by-one error. Change the loop condition to n <= returns + 1 and see what happens. The program will process a phantom book with data computed out of range. Locate the fault by looking at the value of n at the breakpoint's last stop; you will see n = 4 when you expected n = 3. That is the off-by-one bug lesson 02-02 warned about, caught in ten seconds with the debugger and in half an hour by rereading the code.
- Common Mistakes and Tips
1. Changing code at random. "Let me try < instead of <= and see if it comes out." Sometimes it works and that is the worst thing that can happen, because you will have understood nothing and the bug will come back. Hypothesis first, then verification, then change.
2. Debugging without knowing what result you expect. If you have not worked out the correct answer by hand, you will not recognise it when you see it.
3. Traces without context. System.out.println(fine); in three different places produces three numbers and no information.
4. Leaving the traces in the code. Before calling it finished, search for System.out.println in your file and remove whatever was scaffolding.
5. Placing the breakpoint after the point of failure. If the variable is already wrong when you stop, put it earlier. The goal is to capture the moment it goes wrong.
6. Ignoring the stack trace and running again. The message and the line number are right there, for free. Read them.
7. Step Into on standard library calls. You end up browsing Integer's source code for no reason. Use Step Over for everything you did not write yourself.
8. Debugging the wrong file. If you have made changes and do not recompile (or the IDE does not do it automatically), you will be debugging the previous version. If the lines and the values do not add up, suspect this.
9. Tip: rubber duck debugging. Explain the problem out loud, line by line, to a rubber duck, a plant or an imaginary colleague. It sounds ridiculous and it works: most bugs are discovered at the moment of saying "and here I add the fine to the total, which is declared… ah". Forcing yourself to explain breaks the automatic reading mentioned in section 1.
10. Tip: use version control so you can go back. If you git commit when the program works, you can always compare (git diff) what you have touched since then and go back to the good state (git checkout) if you get lost. A great many bugs are located simply by looking at what changed since the last working version. Git is covered in depth in module 12; with these three commands you already get most of the benefit.
11. Tip: shrink the case. If the bug happens with 100 returns, try to reproduce it with 3. A minimal case is infinitely easier to debug, and the very process of shrinking it often reveals the cause.
12. Tip: rest. A bug you have been hunting for two hours gets found in five minutes the next day. It is not folklore: mental fixation is real and rest breaks it.
Exercises
Exercise 1: instrumenting a loop with traces
Take this code, which is supposed to count how many returns in a batch exceed the minor delay threshold, and add traces to it following the rules from section 4: a location label, the name and value of every relevant variable, and entry and exit traces for the loop. Do not modify the logic yet.
final int LOAN_DAYS = 15;
final int MINOR_THRESHOLD = 7;
int severeCount = 0;
for (int n = 1; n <= 5; n++) {
int elapsedDays = 10 + (n * 23) % 60;
int daysLate = elapsedDays - LOAN_DAYS;
if (daysLate > MINOR_THRESHOLD) {
severeCount++;
}
}
System.out.println("Severe delays: " + severeCount);With the traces in place, run it and answer: is the result correct? Is there any case where daysLate comes out negative, and what does that imply for the count?
Exercise 2: catching the bug with the debugger
The following program is supposed to compute the average fine of a batch of returns. It gives an incorrect result. Locate it with the debugger, following the seven steps of the systematic method, and document in writing: the reproduction data, the expected result calculated by hand, on which line you placed the breakpoint, what you saw in the variables window, what your hypothesis was and what the fix turned out to be.
final double DAILY_RATE = 0.25;
int returns = 4;
double fineSum = 0.0;
int counted = 0;
for (int n = 1; n < returns; n++) {
int daysLate = n * 10;
double fine = daysLate * DAILY_RATE;
fineSum += fine;
counted++;
}
double average = fineSum / counted;
System.out.printf("Average fine over %d returns: %.2f EUR%n", returns, average);Exercise 3: conditional breakpoint
Write a program that goes through 200 simulated days late and accumulates each day's capped fine. Then:
- Place a breakpoint on the accumulator's line.
- Configure it with the condition
daysLate > 30and note on which iterations it stops. - Change it to
fine >= MAX_FINEand note the first day on which it stops. - Add
accumulatedTotalanddaysLateas watch expressions and describe how they evolve. - Use expression evaluation to calculate, without touching the code, what
accumulatedTotal / nwould be at the stop.
Hand in the program and a short description of what you observed at each step.
Solutions
Solution 1
package com.nexussoftware.bibliotech;
public class BiblioTechApp {
public static void main(String[] args) {
final int LOAN_DAYS = 15;
final int MINOR_THRESHOLD = 7;
int severeCount = 0;
// ENTRY trace for the loop
System.out.printf("[loop:IN ] severeCount=%d LOAN_DAYS=%d MINOR_THRESHOLD=%d%n",
severeCount, LOAN_DAYS, MINOR_THRESHOLD);
for (int n = 1; n <= 5; n++) {
int elapsedDays = 10 + (n * 23) % 60;
int daysLate = elapsedDays - LOAN_DAYS;
// Per-iteration trace: location + name=value of everything relevant
System.out.printf("[iter] n=%d elapsed=%d late=%d exceeds=%b%n",
n, elapsedDays, daysLate,
daysLate > MINOR_THRESHOLD);
if (daysLate > MINOR_THRESHOLD) {
severeCount++;
System.out.printf("[iter] n=%d -> severeCount raised to %d%n", n, severeCount);
}
}
// EXIT trace for the loop
System.out.printf("[loop:OUT] severeCount=%d%n", severeCount);
System.out.println("Severe delays: " + severeCount);
}
}Output:
[loop:IN ] severeCount=0 LOAN_DAYS=15 MINOR_THRESHOLD=7 [iter] n=1 elapsed=33 late=18 exceeds=true [iter] n=1 -> severeCount raised to 1 [iter] n=2 elapsed=56 late=41 exceeds=true [iter] n=2 -> severeCount raised to 2 [iter] n=3 elapsed=19 late=4 exceeds=false [iter] n=4 elapsed=42 late=27 exceeds=true [iter] n=4 -> severeCount raised to 3 [iter] n=5 elapsed=25 late=10 exceeds=true [iter] n=5 -> severeCount raised to 4 [loop:OUT] severeCount=4 Severe delays: 4
Answers. The count of 4 is correct for the data generated: iterations 1, 2, 4 and 5 exceed the 7-day threshold and 3 does not. With these particular values daysLate never comes out negative, because the minimum elapsedDays is 19 and that always exceeds the 15 loan days.
But the trace reveals a latent risk: the formula 10 + (n * 23) % 60 can produce values below 15 (for example, with n = 10 it gives 10 + 230 % 60 = 10 + 50 = 60, but with other formulas or ranges it would happen). The code does not clamp the delay to zero, so an elapsedDays lower than 15 would give a negative daysLate. For counting severe cases it makes no difference (a negative never exceeds 7), but if that same daysLate fed the fine calculation, the library would be paying the employee. The lesson: a trace does not just confirm the result, it also exposes unwritten assumptions. The fix is to add the clamp to zero, as everywhere else in BiblioTech:
Solution 2
Step 1: reproduce. Data: returns = 4, simulated delays n * 10. The program always prints:
Step 2: what is expected, by hand. With 4 returns, the delays should be 10, 20, 30 and 40 days, that is, fines of 2.50 + 5.00 + 7.50 + 10.00 = €25.00, and an average of €6.25. The program says €5.00.
Step 3: isolate. Breakpoint on fineSum += fine;.
Step 4: observe. The variables window at each stop:
| Stop | n |
daysLate |
fine |
fineSum after adding |
counted |
|---|---|---|---|---|---|
| 1st | 1 | 10 | 2.5 | 2.5 | 1 |
| 2nd | 2 | 20 | 5.0 | 7.5 | 2 |
| 3rd | 3 | 30 | 7.5 | 15.0 | 3 |
And there is no fourth stop: the loop ends. counted is 3, not 4. The average 15.0 / 3 = €5.00 is arithmetically correct; what is wrong is how many times it iterated.
Step 5: hypothesis. The loop's condition is n < returns with n starting at 1. That covers 1, 2, 3: three iterations, not four. It is a classic off-by-one error, caused by mixing the convention of starting at 1 with the < condition, which is designed for indexes that start at 0.
Verification: a watch on n confirms that the last value it enters the body with is 3.
Step 6: fix. Starting at 1, the correct condition is <=:
package com.nexussoftware.bibliotech;
public class BiblioTechApp {
public static void main(String[] args) {
final double DAILY_RATE = 0.25;
int returns = 4;
double fineSum = 0.0;
int counted = 0;
// FIXED: n starts at 1, so the condition must be <=
for (int n = 1; n <= returns; n++) {
int daysLate = n * 10;
double fine = daysLate * DAILY_RATE;
fineSum += fine;
counted++;
}
// Guard against division by zero: with no returns, there is no average.
if (counted == 0) {
System.out.println("There are no returns to average.");
} else {
double average = fineSum / counted;
System.out.printf("Average fine over %d returns: %.2f EUR%n",
counted, average);
}
}
}Step 7: check.
It matches the manual calculation. And the boundaries get tested: with returns = 0 the loop never runs, counted is 0 and the original version would have done 0.0 / 0, which with double throws no error but produces NaN and an absurd output (Average fine over 0 returns: NaN EUR). The added guard covers it. Notice too that the final printf now uses counted instead of returns: reporting what was actually processed, rather than what was meant to be processed, is a cheap defence against this very family of errors.
Solution 3
package com.nexussoftware.bibliotech;
public class BiblioTechApp {
public static void main(String[] args) {
final double DAILY_RATE = 0.25;
final double MAX_FINE = 20.0;
final int SIMULATED_DAYS = 200;
double accumulatedTotal = 0.0;
int cappedDays = 0;
for (int n = 1; n <= SIMULATED_DAYS; n++) {
int daysLate = n;
double fine = daysLate * DAILY_RATE;
if (fine >= MAX_FINE) {
fine = MAX_FINE;
cappedDays++;
}
accumulatedTotal += fine; // <-- BREAKPOINT HERE
}
System.out.printf("Accumulated total: %.2f EUR%n", accumulatedTotal);
System.out.printf("Days with a capped fine: %d%n", cappedDays);
}
}What you observe at each step:
-
Breakpoint with no condition. The program stops 200 times. Unworkable by hand: it is exactly the scenario that motivates conditions.
-
Condition
daysLate > 30. It stops for the first time withn = 31and keeps stopping at 32, 33, 34… up to 200. It is still a lot, but you have already skipped the first 30 iterations without touching the code. To narrow it further,daysLate > 30 && daysLate % 20 == 0stops only at 40, 60, 80, 100… -
Condition
fine >= MAX_FINE. The first stop is atn = 80, because 80 × 0.25 = €20.00, the first day that reaches the cap. It confirms without calculations the figure you already knew from the previous lesson. -
Watches. With
accumulatedTotalanddaysLatewatched you see the evolution at each stop:accumulatedTotalgrows quadratically while the fine rises (each day adds a little more than the previous one) and switches to growing linearly, exactly €20.00 per day, as soon as day 80 is passed. That change of slope is the cap's effect, visible live. -
Expression evaluation. Stopping at
n = 100and evaluatingaccumulatedTotal / ngives the average fine per day up to that point: 1210.00 / 100 = €12.10, a value the program never computes anywhere. That is expression evaluation's great advantage: asking a program new questions without modifying or recompiling it.
The program's final result, for contrast:
Manual verification, which is step 2 of the method applied to this exercise. You do not believe the program, you check it:
- Days 1 to 79: not capped. The sum 1 + 2 + … + 79 is 79 × 80 / 2 = 3160, and multiplied by the €0.25 rate gives €790.00.
- Days 80 to 200: capped at €20.00. That is 200 − 80 + 1 = 121 days, that is, 121 × 20.00 = €2420.00.
- Total: 790.00 + 2420.00 = €3210.00, and
cappedDays= 121. Both match the output.
If they had not matched, the procedure would be exactly the same as in solution 2: a conditional breakpoint at n == 79, evaluate accumulatedTotal and see whether it is €790.00. If it is, the program is fine up to there and the error is in the manual calculation; if it is not, the bug is in the code and you have already cornered it between iteration 1 and 79. When paper and program disagree, one of the two is lying and you have to find out which: never take the program's result as good just because it is the one in front of you.
Conclusion
You no longer depend on rereading the code to understand what it does. You know how to tell compilation, runtime and logical errors apart, and that only the third kind demands real debugging. You know how to write useful traces —with a location label, the name and value of every variable, delimiting the entry and exit of each block— and why they have to be deleted afterwards, pending module 6's logging. You know how to handle the IDE debugger: breakpoints, step over, step into, step out and resume, the variables window, watched expressions, live expression evaluation and, above all, conditional breakpoints, which turn 200 stops into 3. You know how to read a stack trace and keep the line that matters: the first one mentioning your package. And you have a seven-step method —reproduce, define what is expected, isolate by bisection, formulate a hypothesis, verify, fix, check— that replaces blind searching with an investigation.
In the practical case you have caught the trade's two most common loop bugs: the accumulator declared inside the loop and the summary computed inside it, plus the off-by-one variant. All of it with BiblioTechApp, which by now decides, repeats, dispatches options with switch, filters with continue, stops with break and can now, in addition, be inspected from the inside.
All that is left is to put it all together. The next lesson, Project: The BiblioTech Interactive Menu, is the module's integrating lesson: you will build the complete application in iterations —a looping menu with an arrow switch, validation of every input, return registration, loan simulation and a session summary with accumulated statistics— and you will finish by listing precisely what it still cannot do and which module will solve it.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
