The last lesson: everything the book has said about debugging, organised by the question that decides the strategy — is this a compile-time, run-time or logic error? Includes bisection, tracing a loop's condition, reading a stack trace, the minimal test case, and the rubber duck. Follows Think Java 2e, Chapter D (Debugging), Sections D.1-D.3, pp. 327-340, cross-referenced against The Java Tutorials — What Is an Exception?.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter D · Debugging
Sections D.1-D.3 · pp. 327-340
Objectives
This lesson follows Think Java 2e, Chapter D (Debugging), Sections D.1-D.3, pp. 327-340. Everything on these slides can be checked against those pages.
1. Classify an error as compile-time, run-time or logic, and choose a strategy accordingly.
2. Explain why only the first compiler error message is reliable.
3. Use debugging by bisection when a program will not compile at all.
4. Diagnose a hanging program by distinguishing an infinite loop from infinite recursion.
5. Read a stack trace and name the likely cause of the common exceptions.
6. Find a minimal test case, and break a complex expression into temporary variables.
Warm-up
Everything in this appendix has appeared somewhere already.
Discussion prompt
From Lesson 2b: what are the three kinds of error? From Lesson 8a: what does a StackOverflowError mean? And from Lesson 11b: why should you not compare doubles with ==?
Hint: Compile-time, run-time, logic. And missing base case.
Answer:
The three kinds are compile-time, run-time and logic errors. A StackOverflowError usually means an infinite recursion — a missing or unreachable base case. And doubles are approximate, so == fails on values that ought to be equal.
Although there are debugging suggestions throughout the book, we thought it would be useful to say more in an appendix. If you are having a hard time debugging, you might want to review this appendix from time to time. It is a reference, not a narrative — and this deck is the map of it.
Concept
The best debugging strategy depends on what kind of error you have. That one question splits the whole appendix into three, and answering it first saves the most time.
Figure (svg): Three boxes showing compile-time, run-time and logic errors with an example of each
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter D (Debugging), Sections D.1-D.3, pp. 327-340 — Appendix D begins on printed page 327.
Section
Section D.1
Concept
If the compiler reports 100 error messages, that doesn't mean there are 100 errors in your program. When the compiler encounters an error, it often gets thrown off track for a while. It tries to recover and pick up again after the first error, but sometimes it reports spurious errors.
Foo.java:12: error: ';' expected
Foo.java:14: error: illegal start of expression
Foo.java:14: error: ';' expected
Foo.java:19: error: class, interface, or enum expected
... 96 more| message | trustworthy? |
|---|---|
| the first | yes |
| the rest | possibly spurious |
| the count | not the number of errors |
Only the first error message is truly reliable. We suggest that you fix only one error at a time and then recompile the program. You may find that one semicolon or brace fixes 100 errors.
Notation
The line number is a starting point, and the appendix is precise about what it really means.
Annotate
Read the message, look before the line it names, and fix one thing at a time. Three habits, and together they turn a hundred messages into one edit.
Worked example
Now, start looking for common syntax errors. The list is worth reading through once, slowly.
1. parentheses and brackets balanced and properly nested
2. uppercase letters are not the same as lowercase
3. semicolons at the end of statements (and NOT after braces)
4. matching quotation marks - double for strings, single for chars
5. assignment: the type on the left matches the type on the right
6. method calls: arguments in the right order, with the right types
7. value methods: use the result. void methods: do not
8. instance methods: on an object. static methods: name the class
9. instance variables cannot be used from a static context| item | the lesson that covered it |
|---|---|
| braces and nesting | 5a — always write the braces |
| case sensitivity | 1b |
| semicolons | 1a |
| single vs double quotes | 6b — char against String |
| types on both sides of = | 2a |
| argument order | 4a — two ints are easy to swap |
| void methods return nothing | 4b |
| static context | 13b — the two error messages |
Check the structure first.
Why: All method definitions should be nested within a class definition. All program statements should be within a method definition.
Then the punctuation.
Why: Semicolons, quotes, and — item 3's parenthesis — no semicolons after curly braces.
Then the types.
Why: Items 5, 6 and 7 are all does this expression have the type this position needs.
Then the static rules.
Why: **If you try that in a static method — with or without this — you get a message like non-static variable x cannot be referenced from a static context.**
Verify: Notice that every item on the list appeared in an earlier lesson.
Why: This appendix is a summary, not new material. Its value is being a checklist you can work through when you are frustrated — which is exactly when you stop thinking systematically.
Prediction
The compiler reports 100 errors.
Foo.java:12: error: ';' expected
... 99 more messages| messages | actual errors |
|---|---|
| 100 | ? |
Predict first
How many errors are probably in the program?
Correct: Possibly one — a single semicolon or brace can produce a hundred messages
Why: When the compiler encounters an error, it often gets thrown off track for a while. Only the first error message is truly reliable. You may find that one semicolon or brace fixes 100 errors. Fix one, recompile, and look at the new first message.
Concept
The best kind of debugging is the kind you don't have to do because you avoid making errors in the first place.
| technique | from | what it prevents |
|---|---|---|
| incremental development | Lesson 4b | not knowing where the error is |
| always writing braces | Lesson 5a | a whole class of scope bugs |
| named constants | Lesson 3a | magic numbers in two places |
| automated tests | Lesson A | a change breaking something silently |
Incremental development can help. The key is to start with a working program and add small amounts of code at a time. When there is an error, you will have a pretty good idea of where it is. That is the whole strategy in one sentence: make the search space small before you need to search it.
Trap
Some error messages come with unreliable advice.
error: class Golfer must be declared abstract. It does not
define int compareTo(java.lang.Object) from interface
java.lang.Comparable.
// so you declare Golfer abstract... and now nothing works| the compiler suggests | what is actually wrong |
|---|---|
| declare Golfer abstract | Golfer is missing a compareTo method |
| does the suggestion compile? | yes |
| does it fix the problem? | no |
It sounds like the compiler is telling you to declare Golfer as an abstract class, and if you are reading this book, you probably don't know what that is or how to do it. Fortunately, the compiler is wrong.
Treat the message as evidence, not instruction.
// The solution in this case is to make sure Golfer has a
// method called compareTo that takes an Object as a parameter.
public int compareTo(Object o) { ... }| what a message gives you | reliable? |
|---|---|
| that something is wrong | yes |
| roughly where it was noticed | yes |
| what the problem is | usually |
| what to do about it | not reliably |
Don't let the compiler lead you by the nose. Error messages give you evidence that something is wrong, but the remedies they suggest are unreliable. A compiler knows what does not fit; it does not know what you meant.
Definition probe
The strategy depends on the answer.
Sort into buckets
Sort each symptom.
Prediction
The message names line 14.
11: int x = 5
12: int y = 3;
13:
14: System.out.println(x + y);
// error reported here| detail | |
|---|---|
| line named | 14 |
| line 11 | no semicolon |
Predict first
Where should you look first?
Correct: Before line 14 — the compiler reports where it noticed the problem, not where it is
Why: It tells you where the compiler was when it noticed a problem, which is not necessarily where the error is. Generally, the error will be prior to the location of the error message. A missing semicolon lets the compiler read on until something no longer makes sense.
Explain it to yourself
It seems slower than fixing several.
Discussion prompt
The appendix recommends fixing one error and recompiling. Why is that faster than working through the list?
Hint: How many of the messages are real?
Answer:
Because most of the messages may not be real errors. Fixing them means changing code that was correct, which introduces genuine errors where there were none.
And the first fix changes everything after it. Recompiling after one edit gives you a fresh, accurate first message rather than a stale list.
It is incremental development applied to debugging — one change, one check. The instinct to batch the work is exactly what makes it take longer.
Section
Section D.1
Concept
If the compiler says there is an error and you don't see it, that might be because you and the compiler are not looking at the same code.
// the test:
// put an obvious, deliberate syntax error at the very
// top of the file, then compile again.
xyzzy!!!
public class Bob {| result | means |
|---|---|
| the compiler reports the new error | you are editing the file it compiles |
| it does not | something is wrong with your setup |
This situation is often the result of having multiple copies of the same program. You might be editing one version of the file but compiling a different version. If you are not sure, try putting an obvious and deliberate syntax error right at the beginning of the program.
Notation
If you have examined the code thoroughly, and you are sure the compiler is compiling the right source file, it is time for desperate measures.
Annotate
Bisection needs no understanding of the code at all. That is exactly why it works when everything else has failed: it replaces insight with a procedure that cannot get stuck.
Worked example
A 400-line file will not compile and the message makes no sense. Six steps find the line.
# step 1: copy Bob.java to Bob.java.old
# step 2: delete lines 200-400. compile.| step | lines remaining | compiles? | the error is in |
|---|---|---|---|
| 1 | 1–200 | yes | 201–400 |
| 2 | 1–300 | no | 201–300 |
| 3 | 1–250 | no | 201–250 |
| 4 | 1–225 | yes | 226–250 |
| 5 | 1–238 | no | 226–238 |
| 6 | narrowing | — | one line |
Delete half, compile.
Why: Whether it compiles tells you which half contains the error.
Keep the half that contains it.
Why: And halve again.
About log₂ 400 ≈ 9 steps.
Why: Fewer in practice, because the region stops being plausible sooner.
Then restore.
Why: Start bringing back the code you deleted, a little bit at a time.
Verify: Notice that step 1's answer is no error in the first half even though the first half might not be valid code on its own.
Why: Deleting half a file often creates new errors — an unclosed brace, a missing method. In practice you delete whole methods rather than arbitrary line ranges, which keeps each version syntactically plausible and the answers meaningful.
Prediction
The compiler reports an error you cannot find.
// put xyzzy!!! at the top of the file and compile| the compiler | conclusion |
|---|---|
| reports the new error | ? |
| does not | ? |
Predict first
What does this test tell you?
Correct: Whether the compiler is reading the file you are editing
Why: If the compiler doesn't find the new error, there is probably something wrong with the way you set up the development environment. The usual cause is multiple copies of the same program — editing one file and compiling another, which no amount of staring at the code will reveal.
Concept
It works for other programming languages too — and for a good deal more than languages.
| what is broken | what you bisect |
|---|---|
| a file that will not compile | the lines of the file |
| a config that will not load | the settings |
| a page that renders wrongly | the elements |
| a program that used to work | the commits — git bisect |
| a sorted array | the range — Lesson 12b |
Every row is the same algorithm: split the search space, ask one yes-or-no question, discard half. Lesson 12b introduced it for finding a card; this is the same log₂ n saving applied to finding a mistake.
Trap
The technique deletes most of your code.
# delete lines 200-400. compile. delete lines 100-200.
# compile. delete lines 50-100...
# ...and now you cannot remember what was in any of them| without a backup | consequence |
|---|---|
| each step destroys code | irreversibly |
| after six steps | most of the file is gone |
| finding the bug | no longer helps |
You will have located the error precisely, in a file that no longer contains your program. The procedure only works because the deletions are recoverable.
Make a backup first, every time.
# step 0, always:
cp Bob.java Bob.java.old
# now delete freely - the original is safe| with a backup | |
|---|---|
| deleting half | safe |
| restoring | copy back from .old |
| if you get lost | start over |
Make a backup of the file you are working on is step one of the appendix's own instructions, and it is not a formality. Better still, use version control — then every intermediate state is recoverable and the whole technique becomes routine.
Prediction
A 1000-line file.
// each step halves the remaining code| remaining | after |
|---|---|
| 1000 | start |
| 500 | 1 step |
| 1 | ? |
Predict first
Roughly how many steps to reach one line?
Correct: About 10
Why: Halving a thousand repeatedly reaches one in about ten steps, because log₂ 1000 is close to 10 — exactly the arithmetic from Lesson 12b's binary search. This process is ugly, but it goes faster than you might think and is very reliable.
Fill the middle
Before deleting anything.
Fill in the blanks
# 1. make a backup of the file
# 2. delete about half the code and compile
Why: The backup is what makes the deletions safe, and halving is what makes the search fast — about ten steps for a thousand lines. Whether the reduced version compiles tells you which half contains the error, with no understanding of the code required.
Real world
It works for other programming languages too!
Discussion prompt
The technique needs only a yes-or-no test and a divisible search space. Where else does that description fit?
Hint: A program that used to work.
Answer:
Version history. git bisect finds the commit that broke something by checking out the midpoint of a range and asking does it work here? — a hundred commits in about seven builds.
Configuration, data and input: comment out half the settings, feed in half the records, delete half a document. Anything with a reproducible failure and a splittable input.
And it needs no understanding at all, which is why it works precisely when you have run out of ideas. A procedure that cannot get stuck is worth more than insight you do not currently have.
Section
Section D.2
Concept
If a program stops and seems to be doing nothing, we say it is hanging. Often that means it is caught in an infinite loop or an infinite recursion.
System.out.println("entering the loop");
while (...) {
...
}
System.out.println("exiting the loop");| you see | conclusion |
|---|---|
| both messages | the loop is not the problem |
| only the first | it is stuck in the loop |
| neither | it never got that far |
Run the program. If you get the first message and not the second, you know where the program is getting stuck. Two print statements, and they turn the program hangs into the program hangs here — which is most of the work.
Notation
The appendix gives a decision procedure, and the order matters.
Annotate
Localise before you diagnose. All three branches start by finding out where the program is, and only then ask why.
Worked example
If you think you have an infinite loop and you know which loop it is, add a print statement at the end of the loop that displays the values of the variables in the condition, and the value of the condition.
while (x > 0 && y < 0) {
// do something to x
// do something to y
System.out.println("x: " + x);
System.out.println("y: " + y);
System.out.println("condition: " + (x > 0 && y < 0));
}| iteration | x | y | condition |
|---|---|---|---|
| 1 | 5 | -3 | true |
| 2 | 4 | -3 | true |
| 3 | 3 | -3 | true |
| … | counting down | never changes | true |
Print the variables in the condition.
Why: Both of them, every time round.
And print the condition itself.
Why: (x > 0 && y < 0) — evaluated, so you see what the loop sees.
Read the output.
Why: Now when you run the program, you see three lines of output for each time through the loop.
Look for what is not moving.
Why: The last time through the loop, the condition should be false. If the loop keeps going, you will see the values of x and y, and you might figure out why they are not getting updated correctly.
Verify: In the trace above, y never changes — so the second half of the condition can never become false.
Why: Printing the condition as well as the variables is the part people skip. A compound condition can be true for a reason you did not expect, and seeing the boolean itself removes the guesswork.
Prediction
The program ran for a while and then crashed.
Exception in thread "main" java.lang.StackOverflowError
at Countdown.countdown(Countdown.java:7)
at Countdown.countdown(Countdown.java:10)
at Countdown.countdown(Countdown.java:10)
... repeated| the stack trace shows | meaning |
|---|---|
| one method, repeated | ? |
Predict first
What is the likely cause?
Correct: Infinite recursion — a missing or unreachable base case
Why: Most of the time, an infinite recursion will cause the program to run for a while and then produce a StackOverflowError. The repeated frames in the trace are the tell: each recursive call adds one, and the stack eventually runs out. Check for a base case, then check that the parameters are moving toward it.
Concept
If you know which method is causing an infinite recursion, check that there is a base case.
public void countdown(int n) {
System.out.println("countdown(" + n + ")"); // print the parameters
if (n == 0) {
return;
}
countdown(n - 1);
}| check | what to look for |
|---|---|
| is there a base case? | a condition that returns without recursing |
| is it reachable? | print the parameters and watch them |
| are they moving toward it? | if not, that is the bug |
There should be a condition that makes the method return without making a recursive invocation. If not, you need to rethink the algorithm and identify a base case. If there is a base case, but the program doesn't seem to be reaching it, add a print statement at the beginning of the method that displays the parameters. Lesson 8a's two requirements, as a diagnostic.
Trap
Print statements everywhere, and no hypothesis.
System.out.println("here 1");
System.out.println("here 2");
System.out.println(x);
System.out.println("here 3");
System.out.println("in the loop");
// ...forty more| problem | consequence |
|---|---|
| no hypothesis | you do not know what would confirm it |
| unlabelled values | which x is that? |
| too much output | the answer is in there somewhere |
One of the problems with using print statements for debugging is that you can end up buried in output. Adding more is the instinct; it is usually the wrong direction.
One question at a time, labelled.
// hypothesis: this loop never exits because y is not updated
System.out.println("x: " + x);
System.out.println("y: " + y);
System.out.println("condition: " + (x > 0 && y < 0));| habit | why |
|---|---|
| a hypothesis first | so you know what would disprove it |
| label every value | x: 5, not 5 |
| print the condition too | compound conditions surprise you |
| remove them afterwards | Lesson 12b's discipline |
As you develop a program, you should write code to generate concise, informative traces of what the program is doing. A trace is a designed artefact, not an accumulation — which is the difference between debugging and flailing.
Definition probe
The symptom decides.
Sort into buckets
Sort each symptom.
Fill the middle
Print the variables and the condition.
Fill in the blanks
while (x > 0 && y < 0) x} > 0 && y < 0));
}
Why: Printing the condition itself, not just its variables, is the part people skip — a compound condition can stay true for a reason that is not obvious from the values alone. The last iteration should show the condition false; if it never does, one of the variables is not being updated.
Socratic
Flow of execution, the third branch.
Discussion prompt
When you cannot localise a hang at all, the appendix suggests printing entering method foo in every method. What does that give you?
Hint: What does the output look like?
Answer:
A trace of each method as it is invoked — so you can see where the program actually got to, and where it stopped.
You can also display the arguments each method receives. When you run the program, check whether the values are reasonable, and check for one of the most common errors — providing arguments in the wrong order.
It is a hand-built call trace, and a debugger gives you the same thing without editing anything — which is Lesson A's argument for learning one. The print version works everywhere, including places a debugger cannot reach.
Section
Section D.2
Concept
When an exception occurs, Java displays a message that includes the name of the exception, the line of the program where the exception occurred, and a stack trace.
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:7)| part | tells you |
|---|---|
| the exception name | what kind of thing went wrong |
| the line number | where it happened |
| the stack trace | how the program got there |
The stack trace includes the method that was running, the method that invoked it, the method that invoked that one, and so on. It is Lesson 4a's stack diagram, printed at the moment of failure — and unlike a compiler message, the line number really is where it happened.
Notation
The first step is to examine the place in the program where the error occurred and see if you can figure out what happened.
Annotate
Every one of these appeared earlier in the book. The appendix's contribution is the second question for each: not what is null but how did it get to be null, which is where the actual bug lives.
Worked example
Now work your way backward through the program and see where the array and the index come from.
System.out.println("index: " + i);
System.out.println("length: " + array.length);
System.out.println(array[i]); // the exception happens here| step | question |
|---|---|
| at the failure | is the index right? is the array the right size? |
| one step back | find the nearest assignment to each |
| if either is a parameter | go to the call site |
| at the call site | where do those values come from? |
Print both values at the point of failure.
Why: Add a print statement immediately before it to display the value of the index and the length of the array.
Decide which one is wrong.
Why: Is the array the right size? Is the index the right value? Usually only one of them is.
Follow it backward.
Why: Find the nearest assignment statement and see if it is doing the right thing.
Cross method boundaries if needed.
Why: If either one is a parameter, go to the place where the method is invoked and see where the values are coming from.
Verify: Notice this is a search backward through the program, one assignment at a time.
Why: The exception tells you where the wrong value was used, not where it was produced. Tracing back to the assignment that created it is the whole technique — and it is the same movement as looking before the line a compiler names.
Prediction
An array of objects.
int[] array = new Point[5];
System.out.println(array[0].x);| after new Point[5] | elements |
|---|---|
| array[0] | ? |
Predict first
Why does the second line throw?
Correct: The array's elements are initially null, so array[0] is not an object
Why: Remember that when you declare a variable with an array type, its elements are initially null until you assign a value to them. The array exists with five slots; no Point objects were created — which is Lesson 12b's array of nulls, met here as a diagnosis.
Concept
To simplify the program, scale down the problem the program is working on.
| if the program | try |
|---|---|
| sorts an array | a small array |
| takes input from the user | the simplest input that causes the error |
| has a deeply nested part | rewriting that part with a simpler structure |
| has a large method | splitting it into smaller methods and testing them separately |
The process of finding the minimal test case often leads you to the bug. For example, if you find that a program works when the array has an even number of elements, but not when it has an odd number, that gives you a clue about what is going on. The reduction is not preparation for debugging — it often is the debugging.
Trap
More print statements, on a bigger input.
// sorting 10,000 elements, printing every comparison
// 100,000 lines of output, and the bug is in there somewhere| problem | effect |
|---|---|
| the input is large | the output is enormous |
| everything is printed | nothing stands out |
| scrolling | you lose the beginning |
One of the problems with using print statements for debugging is that you can end up buried in output. The instinct when a trace is not helping is to add more of it, and that makes it worse.
Simplify the output, or simplify the program.
// simplify the output:
// remove or comment out prints that aren't helping,
// combine them, or format them so they are readable
// simplify the program:
// sort a SMALL array
// give the simplest input that causes the error| approach | what it does |
|---|---|
| fewer, better prints | the trace becomes readable |
| a smaller input | the trace becomes short |
| cleaner code | the bug becomes visible |
There are two ways to proceed: either simplify the output or simplify the program. And there is a bonus: reorganizing the program can help you find subtle bugs. If you make a change that you think doesn't affect the program, and it does, that can tip you off.
Definition probe
Each names a different kind of mistake.
Sort into buckets
Sort each situation.
Prediction
It lists more than one method.
at Deck.subdeck(Deck.java:45)
at Deck.mergeSort(Deck.java:88)
at Main.main(Main.java:12)| line | meaning |
|---|---|
| the first | where it failed |
| the rest | ? |
Predict first
What do the lower lines show?
Correct: The chain of calls that led there — mergeSort called subdeck, and main called mergeSort
Why: The stack trace includes the method that was running, the method that invoked it, the method that invoked that one, and so on. It answers how did we get here, which for a method called from several places is often the whole question — Lesson 4a's stack diagram, printed automatically.
Real world
The bug is the same either way.
Discussion prompt
The process of finding the minimal test case often leads you to the bug. Why would shrinking the input reveal anything?
Hint: What can you check by hand?
Answer:
Because a small case can be traced by hand. Five elements you can work through on paper; ten thousand you cannot, so you are reduced to guessing.
And the boundary between working and failing is informative. Works with an even number of elements, fails with an odd one is nearly a diagnosis on its own.
Shrinking is a form of bisection — you are narrowing the space in which the bug can hide, the same move as deleting half a file, applied to the input instead of the code.
Section
Section D.3
Concept
Logic errors are hard to find because the compiler and interpreter provide no information about what is wrong. Only you know what the program is supposed to do, and only you know that it isn't doing it.
// Is there something the program was supposed to do that
// doesn't seem to be happening?
// Is something happening that shouldn't?
// Is a section of code producing an unexpected effect?| question | what to do |
|---|---|
| something is not happening | find that code, check it is executing |
| something happens that should not | find that code, see if it runs when it should not |
| a section behaves unexpectedly | read the documentation for the methods it calls |
The first step is to make a connection between the code and the behavior you get. You need a hypothesis about what the program is actually doing. Not what it should do — what it is doing, which is a different and more useful question.
Notation
The appendix's most striking sentence, and the technique that follows from it.
Annotate
Once you find the discrepancy between your model and reality, you can solve the problem. Finding it is the work; the fix is usually easy afterwards.
Worked example
Here are some common logic errors to check for. Every one appeared earlier in the book.
// integer division always rounds toward zero
// -> use double for fractions; integers for countable
// things, floating-point for measurable things
// floating-point numbers are only approximate
// -> never use == with doubles
// if (Math.abs(d - 1.23) < .000001)
// == on objects checks IDENTITY
// -> use equals for equivalence
// by default, equals checks identity too
// -> override it if you want a different notion
// inheritance can run code you did not realise was there| error | the lesson |
|---|---|
| integer division | 2b |
| comparing doubles with == | 11b |
| == on objects | 9a and 11b |
| the default equals | 11b |
| inherited code running | 14a and 17b |
Integer division rounds toward zero.
Why: Use integers for countable things and floating-point numbers for measurable things — a rule worth more than the arithmetic.
Doubles are approximate.
Why: You should probably never use the == operator with doubles. Compare with a tolerance.
== on objects asks the wrong question.
Why: If you meant to check equivalence, you should use the equals method instead.
And equals defaults to identity.
Why: If you want a different notion of equivalence, you have to override it.
Verify: Count how many of the five you have made at least once while working through this course.
Why: These are not obscure edge cases. They are the five mistakes that produce a program which compiles, runs, and quietly gives the wrong answer — which is why they get their own list.
Prediction
Same precedence, left to right.
double y = x / 2 * Math.PI;| operator | precedence |
|---|---|
| / | same as * |
| * | same as / |
Predict first
How is it grouped?
Correct: (x / 2) × π
Why: Multiplication and division have the same precedence, and they are evaluated from left to right. So the division happens first, and the result is π² times too large. Nothing reports it — which is the definition of a logic error, and why parentheses are worth the two characters.
Concept
Writing complex expressions is fine as long as they are readable, but they can be hard to debug.
double halfWidth = 0.5 * rect.getWidth();
double halfHeight = 0.5 * rect.getHeight();
int dx = (int) Math.round(halfWidth);
int dy = (int) Math.round(halfHeight);
rect.translate(dx, dy);| one expression | temporary variables | |
|---|---|---|
| readable | less | more — the names document it |
| can you print an intermediate value? | no | yes |
| can you check the types? | hard | they are written down |
| where did it go wrong? | somewhere in there | one line |
The second version is easier to read, partly because the variable names provide additional documentation. It's also easier to debug, because you can check the types of the temporary variables and display their values. The same argument applies to a complex return: now you have the opportunity to display any of the intermediate variables before returning.
Trap
Multiplication and division have the same precedence.
// to evaluate x / (2 pi):
double y = x / 2 * Math.PI; // WRONG - this is (x/2) * pi
double y = x / (2 * Math.PI); // correct| expression | evaluates as | wanted |
|---|---|---|
| x / 2 * Math.PI | (x / 2) × π | x / (2π) |
| why | same precedence, left to right | — |
| does it compile? | yes | — |
That is not correct, because multiplication and division have the same precedence, and they are evaluated from left to right. The program runs and produces a number that is wrong by a factor of π² — a logic error of exactly the kind nothing reports.
Use parentheses, even when you are sure.
double y = x / (2 * Math.PI);| benefit | detail |
|---|---|
| correct | the grouping is explicit |
| readable | no memorisation required |
| cheap | two characters |
This version is correct, and more readable for other people who haven't memorized the order of operations. If you are not sure of the order of operations, check the documentation, or use parentheses to make it explicit — and the parentheses are the better answer even when you are sure.
Definition probe
Five that compile and run.
Sort into buckets
Sort each symptom.
Fill the middle
Never with ==.
Fill in the blanks
if (Math.abs(d - 1.23) < .000001) ___
Why: The absolute value of the difference is compared against a small tolerance, because floating-point values are approximate and two calculations that should agree often differ in the last bits. Lesson 11b used exactly this pattern for a Time class's equals method.
Real world
We're not kidding, it works!
Discussion prompt
The appendix recommends explaining your problem to a rubber duck, in earnest. Why would talking to an inanimate object help?
Hint: What does explaining require?
Answer:
Because explaining forces you to be explicit. You have to say what the code is supposed to do, what it actually does, and why you believed those were the same — and the gap usually appears while you are saying it.
By the time you explain the problem to someone, you might see the answer. The appendix says this is so common it has a name, and the duck is just a listener who is always available.
It also stops you skipping steps. Silent thinking glosses over the assumption you never questioned; saying it out loud does not — which is why the technique survives being faintly ridiculous.
Comparison
Fill the blanks.
Comparison matrix
| compile-time | run-time | logic | |
|---|---|---|---|
| what is wrong | the syntax | something fails while running | the program does the wrong thing |
| who tells you | the compiler | the program, with an exception | nobody |
| example | a missing semicolon | StackOverflowError | an expression evaluated in the wrong order |
| first move | read the first message, look before that line | read the stack trace | form a hypothesis about what it is doing |
| hardest because | messages can be spurious | the cause may be far from the symptom | only you know it is wrong |
The bottom-right cell is why the third column gets its own section. A compiler error stops you; a logic error lets you carry on being wrong — sometimes for a long time.
Pattern
Localise, then diagnose — and halve the search space whenever you can.
// 1. which kind of error is it?
// compile-time -> read the FIRST message, look BEFORE that line
// run-time -> read the stack trace
// logic -> form a hypothesis about what it IS doing
// 2. localise before diagnosing
System.out.println("entering the loop");
...
System.out.println("exiting the loop");
// 3. halve the search space
// the file -> bisection
// the input -> the minimal test case
// the code -> temporary variables
// 4. when stuck: walk away, then explain it out loud| technique | answers |
|---|---|
| bracketing print statements | where did it stop? |
| printing the loop condition | why is it still true? |
| printing the parameters | are they reaching the base case? |
| bisection | which half contains it? |
| the minimal test case | what exactly triggers it? |
| temporary variables | which sub-expression is wrong? |
== with doubles, and never with objects when you mean equivalence.Check
Work it out before you click.
// the compiler reports 100 errors| message | reliable? |
|---|---|
| the first | yes |
| the others | ? |
Check your understanding
What should you do?
Answer: A
Why: Only the first error message is truly reliable. We suggest that you fix only one error at a time and then recompile the program. You may find that one semicolon or brace fixes 100 errors. Working through the whole list means changing code that was correct, which creates real errors where there were none.
Check
Work it out before you click.
// the program runs for a few seconds, then:
Exception in thread "main" java.lang.StackOverflowError
at Series.fibonacci(Series.java:8)
at Series.fibonacci(Series.java:8)
... repeated| the trace shows | meaning |
|---|---|
| one method repeated | ? |
Check your understanding
What is the likely cause, and what do you check first?
Answer: A
Why: Most of the time, an infinite recursion will cause the program to run for a while and then produce a StackOverflowError. The two checks are Lesson 8a's requirements: check that there is a base case, and if there is one but the program is not reaching it, add a print statement at the beginning of the method that displays the parameters and watch whether they are moving toward it.
Check
Work it out before you click.
double y = x / 2 * Math.PI;
// intended: x / (2 * pi)| grouping | |
|---|---|
| as written | ? |
Check your understanding
What is wrong, and how do you fix it?
Answer: A
Why: That is not correct, because multiplication and division have the same precedence, and they are evaluated from left to right. The fix is x / (2 * Math.PI) — and the appendix's advice generalises: if you are not sure of the order of operations, check the documentation, or use parentheses to make it explicit, which is also more readable for anyone who has not memorised the rules.
x is a double, so the division is floating-point — the grouping is the problem, not truncation.Real world
First, get away from the computer for a few minutes. Computers emit waves that affect the brain, causing the following symptoms: frustration and rage; superstitious beliefs; sour grapes.
Discussion prompt
The appendix is joking about the waves and serious about the advice. Why does stepping away actually help?
Hint: What happens to your thinking after an hour of failure?
Answer:
Because frustration narrows what you consider. After an hour you are re-checking the same three lines and have stopped questioning the assumption that is actually wrong.
Sometimes it just takes time to find a bug. People often find bugs when they let their mind wander. Good places to find bugs are buses, showers, and bed. That is a widely reported experience, not a joke.
And the symptoms it lists are real diagnostic signs. The computer hates me and this program is lame anyway are both ways of saying you have stopped debugging — which is precisely when a walk is more productive than another hour.
Commit first
Commit to an answer and to your confidence.
Predict first
The compiler names line 42 as the site of a syntax error. Where should you look?
Correct: At line 42 and, especially, before it — the compiler reports where it noticed the problem
Why: Actually, it tells you where the compiler was when it noticed a problem, which is not necessarily where the error is. Generally, the error will be prior to the location of the error message, but in some cases it will be somewhere else entirely. A missing semicolon lets the compiler keep reading until something no longer fits, which may be a line or two later. The fourth option is not absurd — the appendix notes that if you get an error message at a method invocation, the actual error may be in the method definition itself — but look earlier is the general rule, and broaden the search is the fallback.
Explain it
Two minutes, out loud — and it is also the technique.
Discussion prompt
A classmate says my program doesn't work and does not know where to start. Give them the first three questions to answer.
Hint: The kind of error decides everything else.
Answer:
First: which kind of error is it? Does it fail to compile, crash while running, or run fine and give the wrong answer? Each has a completely different strategy.
*Second: where is it? If it compiles, read the stack trace or bracket a suspect section with print statements. Turning it doesn't work into it stops here is most of the job.*
Third: what is the smallest input that still fails? Shrinking the test case often finds the bug on its own. And then explain the whole thing out loud — the appendix means the rubber duck seriously, and by the time you have finished explaining you often see it.
Exit ticket
One question before you close the deck — and the course.
Predict first
Why does the best debugging strategy depend on the kind of error?
Correct: Because each kind gives you different information: the compiler names a location, an exception gives a stack trace, and a logic error reports nothing at all
Why: The best debugging strategy depends on what kind of error you have. A compile-time error hands you a line number — approximate, and before it rather than at it. A run-time error hands you an exception name, a line, and a stack trace showing how you got there. A logic error hands you nothing, because only you know what the program is supposed to do, and only you know that it isn't doing it — which is why that section starts by asking you to form a hypothesis about what the program is actually doing, and why it warns that the problem might not actually be the program; it might be in your head.
Connect it up
One page, from memory — the last one.
Draw it
Draw three columns headed compile-time, run-time and logic. Under each, write what is wrong, who tells you, one example, and the first move. Then, under run-time, add the five common exceptions with a one-line cause for each. Under compile-time, write the bisection procedure in four steps, starting with the backup. Under logic, list the five common errors — integer division, doubles with ==, objects with ==, the default equals, and inherited code. Finish by writing the four things to tell someone when you ask for help.
Recap
The last appendix, and the last lesson: everything the book has said about debugging, in one place.
| if you remember one thing | it is this |
|---|---|
| about strategy | which kind of error is it? |
| about method | localise before you diagnose |
| about being stuck | explain it out loud; the gap appears as you speak |
Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.