The double type and the integer-division trap it is meant to solve, why 0.1 added ten times is not 1.0, what the plus operator does to strings, the order of operations, and the three kinds of error every programmer learns to tell apart. Follows Think Java 2e, Chapter 2 (Variables and Operators), Sections 2.6-2.10, pp. 23-29, cross-referenced against The Java Tutorials — Primitive Data Types.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 2 · Variables and Operators
Sections 2.6-2.10 · pp. 23-29
Objectives
This lesson follows Think Java 2e, Chapter 2 (Variables and Operators), Sections 2.6-2.10, pp. 23-29. Everything on these slides can be checked against those pages.
1. Declare and use double variables, and say when Java performs floating-point rather than integer division.
2. Explain why double y = 1 / 3; gives 0.0, and fix it.
3. Say what a rounding error is and name a case where you should use integers instead of doubles.
4. Predict the result of the + operator when strings and numbers are mixed.
5. Apply Java's order of operations, and use parentheses deliberately rather than defensively.
6. Classify any error as compile-time, run-time or logic, and say which of the three the compiler can help with.
Warm-up
One thing from the previous lesson, because this one exists to fix it.
Discussion prompt
From memory: what does System.out.println(59 / 60); display, and why? Then guess — what would you have to change to get 0.98333 instead?
Hint: The answer to the first part is a single character long.
Answer:
It displays 0. Both operands are integers, so Java performs integer division, which rounds toward zero and discards the remainder.
To get the fraction you need a different type — one that can hold values with decimal places. That type is double, and Section 2.6 is about it. Your guess was probably 'use decimals somewhere', which is right; the interesting part is exactly where.
Concept
Two new capabilities and one new way of thinking. The capabilities are decimal numbers and joining strings together. The way of thinking is the three-way classification of errors, which you will use for the rest of your programming life.
Figure (svg): Three boxes naming compile-time errors, run-time errors and logic errors, with when each one appears
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 2 (Variables and Operators), Sections 2.6-2.10, pp. 23-29 — Sections 2.6-2.10, printed pages 23-29.
Section
Section 2.6
Concept
A more general solution to the integer-division problem is to use floating-point numbers, which represent values with decimal places. In Java the default floating-point type is called double, short for double-precision.
double pi;
pi = 3.14159;
double minute = 59.0;
System.out.print("Fraction of the hour that has passed: ");
System.out.println(minute / 60.0);| expression | operand types | kind of division | result |
|---|---|---|---|
| 59 / 60 | int, int | integer | 0 |
| 59.0 / 60.0 | double, double | floating-point | 0.9833333333333333 |
| 59.0 / 60 | double, int | floating-point | 0.9833333333333333 |
Java performs floating-point division when one or more operands are double values. One is enough — which is both convenient and, as the next slides show, a source of confusion.
Notation
The single most useful thing to internalise in this chapter: look at the operands, not at the variable you are assigning to.
Annotate
60.0 instead of 60 or by declaring the variable as a double.Read this table left to right, not right to left. The question is never 'what do I want out?' but 'what did I put in?'
Worked example
This looks correct, compiles cleanly, and produces the wrong answer. Work out where the information is lost.
double y = 1 / 3;
System.out.println(y); // displays 0.0| step | what happens | value |
|---|---|---|
| 1 / 3 | both operands are int, so integer division | 0 (an int) |
| assignment to a double | Java converts the int 0 to a double | 0.0 |
| println(y) | displays the double | 0.0 |
Read the right-hand side on its own, ignoring the variable.
Why: 1 / 3 — two integers. Java has no reason to think otherwise.
Perform integer division.
Why: The result is the int 0. The decimal part never existed; it was not computed and then rounded.
Only now look at the left-hand side.
Why: y is a double, so Java converts 0 to 0.0 — faithfully preserving a value that was already wrong.
Fix it by changing the operands.
Why: double y = 1.0 / 3.0; sets y to 0.3333333333333333, as expected.
Verify: Run both versions and expect 0.0 from the first and 0.3333333333333333 from the second.
Why: The lesson is the ordering: the right-hand side is fully evaluated BEFORE the assignment happens, so the variable's type cannot rescue an expression that has already lost its precision.
As a matter of style, always assign floating-point values to floating-point variables. The compiler will not make you, but you never know when a simple mistake will come back to haunt you.
Prediction
Read the right-hand side first, on its own.
double y = 5 / 2;| step | value | type |
|---|---|---|
| 5 / 2 | 2 | int — integer division |
| converted on assignment | 2.0 | double |
Predict first
What value does y hold?
Correct: 2.0
Why: Both operands are integers so Java performs integer division, giving the int 2, and only then converts that to the double 2.0 for storage. The declared type of y affects how the value is stored, never how the expression on the right is computed.
Concept
Strictly speaking you are not allowed to make assignments between types. In many cases Java converts automatically anyway — and that leniency is convenient right up to the moment it hides a bug.
| statement | legal? | what Java does |
|---|---|---|
| double y = 1; | yes | converts the int 1 to the double 1.0 — legal, but bad style |
| int x = 1.1; | no | compiler error — this would lose information, so Java refuses |
| double y = 1 / 3; | yes | computes the int 0, then converts to 0.0 — legal and almost certainly wrong |
| double y = 1.0 / 3.0; | yes | floating-point division throughout — correct |
Notice the asymmetry. Java will widen an int to a double without complaint, because nothing is lost. It refuses to narrow a double to an int, because something would be. The dangerous row is the third, where the loss happened before the conversion Java was willing to make.
Trap
The assumption. Declaring the variable as a double makes the arithmetic floating-point.
double average = 7 / 2;
System.out.println(average); // expected 3.5| stage | types involved | value |
|---|---|---|
| evaluate 7 / 2 | int, int | 3 |
| assign to a double | int converted to double | 3.0 |
| display | double | 3.0 — not 3.5 |
The precision was gone before the assignment was even considered. Java did convert to a double, exactly as promised — it converted the wrong number.
The fix. Make at least one operand a double, so the division itself is floating-point.
double average = 7.0 / 2;
System.out.println(average); // 3.5| stage | types involved | value |
|---|---|---|
| evaluate 7.0 / 2 | double, int -> floating-point | 3.5 |
| assign to a double | already a double | 3.5 |
| display | double | 3.5 |
One character. Writing 7.0 instead of 7 changes which division Java performs, and that is the only thing that ever changes it.
Definition probe
Look only at the operands.
Sort into buckets
Sort each expression by the kind of division Java performs.
Fill the middle
Make this compute 3.5 rather than 3.0, changing as little as possible.
Fill in the blanks
double average = 7.0 / 2;
Why: Writing 7.0 makes one operand a double, which is enough to make the whole division floating-point and give 3.5. Changing the declared type of the variable would not help, because the expression on the right is evaluated before the assignment ever happens.
Counterexample
You have just seen it hide a bug. Argue the other side.
Discussion prompt
Java silently converts double y = 1; into double y = 1.0;. Given that this leniency causes the 1 / 3 bug, why do you think the language designers allowed it? What would writing Java be like if every conversion had to be explicit?
Hint: Think about how often you would have to write .0.
Answer:
Without it, every mixed expression would need a manual conversion — x * 2 would be illegal for a double x, and you would write x * 2.0 everywhere. The noise would be constant and most of it would be pointless.
The design rule Java actually follows is: widening conversions are automatic, narrowing ones are not. int to double loses nothing, so it is silent; double to int loses the fractional part, so it must be written out (Chapter 3 shows how). The 1 / 3 bug slips through because the loss happens in the division, before any conversion is involved — so the rule is not violated, merely unhelpful.
Section
Section 2.7
Concept
Some numbers, like reasonably sized integers, can be represented exactly. But repeating fractions like one third, and irrational numbers like pi, cannot. To represent these, computers round off to the nearest floating-point number — and the difference is called rounding error.
System.out.println(0.1 * 10);
System.out.println(0.1 + 0.1 + 0.1 + 0.1 + 0.1
+ 0.1 + 0.1 + 0.1 + 0.1 + 0.1);| statement | output on many machines |
|---|---|
| 0.1 * 10 | 1.0 |
| 0.1 added ten times | 0.9999999999999999 |
| mathematically | these are the same number |
rounding error — The difference between the number we want and the floating-point number we actually get.
The problem is that 0.1 is a repeating fraction when converted into binary, so what is stored is only approximate. Adding the approximations up makes the errors accumulate.
Picture it
One third is awkward in decimal — 0.333... never terminates. One tenth is awkward in binary for exactly the same reason.
Figure (svg): Two panels comparing one third written in decimal with one tenth written in binary, both non-terminating
Nothing is broken. A number that needs infinitely many digits in a base cannot be written exactly in finitely many — and a computer has finitely many. The surprise is only that the awkward numbers in binary are different from the awkward numbers in decimal.
Worked example
Think Java gives a concrete case where rounding error is not acceptable, and the fix is to change the type rather than to add more decimal places.
// potential rounding error:
double balance = 123.45;
// exact, as long as the amount fits in an int:
int balance = 12345; // total number of cents| representation | stores | exact? | limit |
|---|---|---|---|
| double balance = 123.45; | an approximation of 123.45 | no | errors accumulate over many operations |
| int balance = 12345; | the number of cents | yes | about 2 billion cents |
Ask whether the quantity is really continuous.
Why: Money is not: there is a smallest unit, the cent. Nothing between 1 and 2 cents exists.
Represent it as a whole number of the smallest unit.
Why: Store cents in an int rather than pounds or dollars in a double.
Check the range.
Why: This works as long as the number of cents does not exceed the largest int, about 2 billion.
Convert only for display.
Why: Divide by 100 at the moment you print, not while you compute.
Verify: Add 0.10 to a double balance ten times and print it; then add 10 to an int balance of cents ten times and print that.
Why: The double drifts; the int does not. Think Java's summary of the stakes is blunt — inaccurate balances mean angry customers and potential lawsuits.
Prediction
One of these two lines surprises people.
System.out.println(0.1 + 0.2);| value | stored as | sum |
|---|---|---|
| 0.1 | slightly more than one tenth | — |
| 0.2 | slightly more than two tenths | — |
| 0.1 + 0.2 | the two approximations added | 0.30000000000000004 |
Predict first
What is displayed on a typical machine?
Correct: 0.30000000000000004
Why: Neither 0.1 nor 0.2 can be stored exactly in binary, so their stored approximations add to something very slightly more than 0.3, and Java prints enough digits to show it. This is the same phenomenon as the ten-additions example: the errors are individually invisible and become visible when combined.
Concept
None of this means doubles are bad. For many applications the benefits far outweigh the costs — the question is whether your quantity is continuous or countable.
| application | use | why |
|---|---|---|
| computer graphics | double | positions are continuous; a tiny error is invisible |
| encryption | integers | an exact value is the whole point |
| statistical analysis | double | measurements are approximate anyway |
| multimedia rendering | double | small errors are below perception |
| money | integers (cents) | there is a smallest unit, and errors accumulate |
| counting things | integers | there is no such thing as 2.5 students |
The rule of thumb: if you need absolute precision, use integers. If the quantity is genuinely continuous and small errors do not accumulate into something a person would notice, a double is right.
Trap
The natural test. Add a tenth ten times and check whether you have arrived at one.
double total = 0.0;
// ... 0.1 added ten times ...
if (total == 1.0) {
System.out.println("exactly one");
}| what you expect | what is stored | the test |
|---|---|---|
| 1.0 | 0.9999999999999999 | false |
| the message prints | it does not | no error, no warning |
This compiles, runs, and quietly does nothing. It is a logic error — the third kind, which you will meet properly at the end of this lesson.
Two ways out. Either avoid the accumulation entirely, or compare with a tolerance instead of exactly.
// best: do not accumulate rounding error at all
int tenths = 0;
// ... 1 added ten times ...
if (tenths == 10) { ... }
// or: allow a small difference
if (Math.abs(total - 1.0) < 0.000001) { ... }| approach | what it relies on |
|---|---|
| count in integers | exactness — no rounding error is ever introduced |
| compare with a tolerance | the error being smaller than the tolerance you chose |
You will meet if properly in Chapter 5 and Math.abs in Chapter 4. The idea is worth having now: never test two doubles for exact equality unless you can prove no rounding has occurred.
Discrimination
For each quantity, decide which representation you would choose.
Sort into buckets
Sort each quantity by the type you would use to store it.
Socratic
There is an obvious-looking alternative fix. Test it.
Discussion prompt
Instead of storing money as an integer number of cents, why not just use a floating-point type with more digits of precision? Would that solve the problem, or postpone it?
Hint: Ask whether the problem is the number of digits or something else.
Answer:
It postpones it. More precision means a smaller error per operation, but one tenth is still a repeating fraction in binary at any precision — so the error is never zero, and it still accumulates.
Storing cents changes the kind of number, not its size: every value you care about becomes a whole number, and whole numbers within range are stored exactly. That is a solution rather than a mitigation, which is why financial software does it.
Estimation
You add one cent, stored as the double 0.01, a million times to a balance. Estimate the outcome.
Predict first
Roughly what happens to the accumulated total?
Correct: It drifts away from the exact value by a small but growing amount
Why: Each addition introduces a tiny rounding error and they accumulate rather than cancelling, so the total drifts steadily. It does not become wildly wrong, which is precisely what makes it dangerous — the figure still looks plausible, so nothing alerts you until someone reconciles the accounts.
Section
Section 2.8
Concept
In general you cannot perform mathematical operations on strings, even if the strings look like numbers. The + operator is the exception — but it does not add. For strings it performs concatenation, which means joining end to end.
// illegal:
// "Hello" - 1
// "World" / 123
// "Hello" * "World"
// legal:
String greeting = "Hello, " + "World!"; // "Hello, World!"
String name = "Ada";
String personal = "Hello, " + name; // "Hello, Ada"| expression | legal? | result |
|---|---|---|
| "Hello" - 1 | no | subtraction is not defined for strings |
| "World" / 123 | no | division is not defined for strings |
| "Hello, " + "World!" | yes | "Hello, World!" |
| "Hello, " + name | yes | "Hello, Ada" |
concatenation — Joining two strings end to end. In Java this is what the + operator does when its operands are strings.
This is the first operator you have met that means two different things depending on the types of its operands. That flexibility is useful and it is also the source of the next slide's surprise.
Picture it
+ looks at what it has been given and behaves accordingly. Nothing else in this chapter does that.
Figure (svg): A rule card showing that plus adds when both operands are numbers and concatenates when either is a string
The second line is the important one. If either operand is a string, the result is a string — and whatever the other operand was gets converted into text to make that possible.
Worked example
Since addition is defined for both numbers and strings, Java performs automatic conversions you may not expect. Both of these lines are legal and their outputs differ.
System.out.println(1 + 2 + "Hello"); // 3Hello
System.out.println("Hello" + 1 + 2); // Hello12| expression | first step | second step | output |
|---|---|---|---|
| 1 + 2 + "Hello" | 1 + 2 is 3 (both numbers) | 3 + "Hello" is "3Hello" | 3Hello |
| "Hello" + 1 + 2 | "Hello" + 1 is "Hello1" | "Hello1" + 2 is "Hello12" | Hello12 |
Java executes these operations from left to right.
Why: Both expressions have two + operators of equal precedence, so the leftmost happens first.
In the first line, the leftmost + has two numbers.
Why: So it adds: 1 + 2 gives 3. Only then does a string appear.
In the second line, the leftmost + has a string on its left.
Why: So it concatenates: "Hello" + 1 gives "Hello1", and the 1 has become text.
Once a string enters, everything after it is concatenation.
Why: The result of a concatenation is a string, so the next + has a string operand too.
Verify: Run both lines and expect 3Hello and Hello12.
Why: If you predicted Hello3 for the second, you assumed Java would add the numbers first wherever they appear — but position matters, because evaluation is strictly left to right.
The practical consequence: when you want numbers added inside a concatenation, put them in parentheses.
Prediction
Evaluate strictly left to right.
System.out.println("Sum: " + 3 + 4);| step | operands | result |
|---|---|---|
| 1 | "Sum: " and 3 | "Sum: 3" |
| 2 | "Sum: 3" and 4 | "Sum: 34" |
Predict first
What appears on the screen?
Correct: Sum: 34
Why: The leftmost + has a string on its left, so it concatenates rather than adds, turning 3 into text. The result is a string, so the second + concatenates too. To get Sum: 7 you would write "Sum: " + (3 + 4), forcing the addition to happen first.
Concept
Once you know the rule, the fix is mechanical: parentheses make the addition happen before any string gets involved.
| expression | output | why |
|---|---|---|
| "Total: " + 1 + 2 | Total: 12 | concatenation all the way — the numbers become text |
| "Total: " + (1 + 2) | Total: 3 | the parentheses add first, then the result is concatenated |
| 1 + 2 + " items" | 3 items | the numbers are added before the string appears |
| "" + 1 + 2 | 12 | an empty string is enough to turn everything into concatenation |
The third row is worth noticing: sometimes the ordering already does what you want, and no parentheses are needed. But writing them anyway makes the intent obvious to a reader, which is a good enough reason.
Trap
The natural attempt. Label a sum by putting the text first.
int a = 20, b = 22;
System.out.println("The answer is " + a + b);| step | expression | result |
|---|---|---|
| leftmost + | "The answer is " + a | "The answer is 20" |
| next + | "The answer is 20" + b | "The answer is 2022" |
| output | — | The answer is 2022 |
No error. The numbers were converted to text and joined, exactly as the rule says — the result is just not what was wanted.
Parenthesise the arithmetic so it happens before any string is involved.
int a = 20, b = 22;
System.out.println("The answer is " + (a + b));| step | expression | result |
|---|---|---|
| parentheses first | a + b | 42 (a number) |
| then the + | "The answer is " + 42 | "The answer is 42" |
| output | — | The answer is 42 |
This is the most common use of parentheses you will write in your first month of Java, and forgetting them produces the most recognisable beginner bug there is: two numbers stuck together.
Ranking
All four are legal. Work out each result, then order them shortest output first.
Put in order
Why: 3x and x3 are both two characters, then x12 is three and x123 is four. The pairs to compare are the first two: putting the string on the right lets the numbers add, while putting it on the left turns every following number into text. Parentheses restore the addition wherever you need it.
Matching
Four expressions using the + operator.
Match the pairs
Why: Two strings concatenate; two numbers add; a string with a number concatenates and converts the number to text. The last one is the subtle case — the numbers add first because the empty string is rightmost, and then the result 3 is converted to the string "3".
Explain it to yourself
Precisely enough to predict any of the cases you have seen.
Discussion prompt
Write a single sentence describing what the + operator does, covering both numbers and strings, and including the order in which multiple + operators are applied.
Hint: Two clauses: what decides the behaviour, and what order they happen in.
Answer:
Something like: + adds when both operands are numbers and concatenates when either is a string, converting the other operand to text; when several + operators appear, they are applied from left to right.
The clause people leave out is the last one, and it is the one that explains why 1 + 2 + "x" and "x" + 1 + 2 differ. Without left-to-right, both expressions would be ambiguous.
Section
Section 2.8, continued
Concept
When more than one operator appears in an expression, they are evaluated according to the order of operations. Generally Java evaluates from left to right, but for numeric operators it follows mathematical conventions.
Figure (svg): Three boxes showing the order of operations: parentheses first, then multiplication and division, then addition and subtraction
1 + 2 * 3 yields 7, not 9, and 2 + 4 / 2 yields 4, not 3.minute * 100 / 60 the multiplication happens first.(1 + 2) * 3 is 9.Oracle publishes a complete table of operator precedence. You do not need to memorise it — but you should internalise these three rules, because they cover nearly everything you will write.
Notation
Think Java makes a specific point here that is easy to skim past, and it is the reason minute * 100 / 60 works at all.
Annotate
100 / 60 is 1 by integer division — destroying the precision and giving 59.(minute * 100) / 60 computes exactly the same thing and says so out loud.Parentheses that do not change the result are still worth writing when they make the intent clear to a reader. That is a real use of them, not a crutch.
Worked example
Apply the rules in order — parentheses, then precedence, then left to right — writing each intermediate form down.
int a = 2, b = 3, c = 4;
System.out.println(a + b * c - (a + b));| step | expression | rule applied |
|---|---|---|
| 0 | a + b * c - (a + b) | as written |
| 1 | 2 + 3 * 4 - (2 + 3) | substitute each variable's value |
| 2 | 2 + 3 * 4 - 5 | parentheses first |
| 3 | 2 + 12 - 5 | multiplication before addition |
| 4 | 14 - 5 | left to right: the addition |
| 5 | 9 | then the subtraction |
Substitute every variable with its current value first.
Why: This is the same first step as any expression evaluation — it removes all the names.
Evaluate anything in parentheses.
Why: (2 + 3) becomes 5. Parentheses always go first, regardless of what is inside them.
Apply the higher-precedence operators.
Why: 3 * 4 becomes 12, before any addition or subtraction happens.
Work left to right through what remains.
Why: 2 + 12 is 14, then 14 - 5 is 9. Addition and subtraction share a precedence level, so order is decided by position.
Verify: Run it and expect 9.
Why: If you got 3, you subtracted before adding; if you got 15, you added before multiplying. Writing each intermediate line out is what makes such a slip visible rather than mysterious.
Prediction
Precedence, then left to right.
System.out.println(10 - 4 - 3);| step | expression |
|---|---|
| as written | 10 - 4 - 3 |
| leftmost first | 6 - 3 |
| result | 3 |
Predict first
What is displayed?
Correct: 3
Why: Both operators are subtraction, so they have equal precedence and are applied left to right: 10 - 4 is 6, then 6 - 3 is 3. Applying them right to left would give 10 - 1 = 9, which is why the left-to-right rule has to be stated rather than assumed.
Concept
Think Java's advice is pragmatic: any time you want to override the order of operations, or you are not sure what it is, use parentheses.
| written | value | clear to a reader? |
|---|---|---|
| minute * 100 / 60 | 98 | correct, but requires knowing the left-to-right rule |
| (minute * 100) / 60 | 98 | yes — the grouping is stated |
| "Total: " + a + b | concatenation twice | misleading — it looks like addition |
| "Total: " + (a + b) | the sum, labelled | yes |
The second and fourth rows are the pattern to adopt. Over time you should internalise these details of the language — but a parenthesis costs nothing and a misread expression can cost an afternoon.
Error analysis
A student's predictions, each with the reasoning that produced it. Find the rule being misapplied in each case.
Annotate
1 + 2 * 3 predicted 9 — evaluated strictly left to right, ignoring precedence. Multiplication happens first, giving 1 + 6 = 7.2 + 4 / 2 predicted 3 — same mistake: 2 + 4 was done first. Division has higher precedence, so it is 2 + 2 = 4.(1 + 2) * 3 predicted 7 — precedence was applied even though parentheses were present. Parentheses beat precedence, always: 3 * 3 = 9."n=" + 1 + 2 predicted n=3 — the numbers were added first because they look like they belong together. But the leftmost + has a string operand, so it concatenates, and everything after becomes text.When an expression surprises you, do not stare at it. Write the intermediate forms down one line at a time, and the step where your prediction diverged will be obvious.
Elimination
Only one of these evaluates to 9.
Eliminate the wrong options
Rule out the three that do not.
Survives elimination: B
Why: Parentheses are evaluated first, so 1 + 2 becomes 3 and then 3 * 3 is 9. Without the parentheses the same characters give 7, which is the clearest demonstration there is that parentheses change meaning rather than merely clarifying it.
Fill the middle
Make this print Total: 42 rather than Total: 2022.
Fill in the blanks
int a = 20, b = 22;
System.out.println("Total: " + (a + b));
Why: Wrapping a + b in parentheses makes the addition happen before the concatenation, so 42 is computed as a number and only then converted to text. Without them, the leftmost + sees a string on its left and concatenates each number in turn, giving 2022.
Edge cases
You have been told to add them freely. Find the cases where they are pure documentation.
Discussion prompt
Give an expression where adding parentheses changes the value, and one where it cannot possibly change the value no matter where you put them. What distinguishes the two?
Hint: Think about whether the operators involved have different precedence.
Answer:
Changes the value: 1 + 2 * 3 versus (1 + 2) * 3 — 7 against 9. The operators have different precedence, so grouping decides which happens first.
Cannot change it: 1 + 2 + 3. All the operators are addition, which is associative, so any grouping gives 6.
The distinguishing feature is whether regrouping changes which operation runs first and whether that matters. Note the second condition — minute * 100 / 60 has equal precedence throughout, yet grouping it as minute * (100 / 60) gives 59 instead of 98, because integer division is not associative once information is being discarded.
Section
Sections 2.9-2.10
Concept
Three kinds of error can occur in a program, and it is useful to distinguish among them in order to track them down more quickly. They differ in when they appear and in how much help you get.
Figure (svg): A pipeline showing where each kind of error appears: compile-time at javac, run-time while the program runs, and logic errors only visible in the output
compile-time error — An error that occurs when you violate the rules of the Java language. The program cannot be compiled.
run-time error — An error that does not appear until after the program has started running; also called an exception.
logic error — The program compiles and runs without any error message, but does not do the right thing.
The third one is the hard one, and Think Java is blunt about why: the compiler and the interpreter cannot help you, since they do not know what the right thing is.
Picture it
The symptom tells you which kind you have, and that tells you where to look.
Figure (svg): Two panels contrasting a compile error message from javac with a run-time exception trace
A logic error produces neither of these. It produces output — plausible, wrong output — and that is the only clue you get.
Worked example
Error messages usually indicate where the error occurred. Sometimes they report where the error was detected, which is not the same place.
1 public class Hello {
2 public static void main(String[] args) {
3 // generate some simple output
4 System.out.println("Hello, World!");
5
6 }| what is wrong | reported at | actually on |
|---|---|---|
| missing semicolon | line 5 | line 4 — usually accurate |
| missing closing brace for main | line 7 — end of file | line 5 |
| the message | reached end of file while parsing | written from the compiler's point of view |
Read the message and the line number.
Why: reached end of file while parsing at line 7 — but the file has only 6 lines.
Recognise the vocabulary as the compiler's, not yours.
Why: Parsing is the process of reading a program before translating it. Reaching the end while still parsing means something was omitted.
Accept that the compiler does not know what or where.
Why: It only knows it ran out of file with work still open. The missing brace could be anywhere above.
Search backwards from the reported line.
Why: Check that every opening brace has a partner, working upward. Consistent indentation makes this a glance rather than a hunt.
Verify: Add the missing brace and recompile; the error disappears and no new one takes its place.
Why: The general lesson: error messages contain useful information, so read them — but do not take them too literally. The line number is where the compiler noticed.
Definition probe
Three kinds, distinguished by when they appear.
Sort into buckets
Sort each situation by the kind of error it is.
Concept
The third kind of error compiles and runs perfectly. Here is a version of Hello World with one, and nothing about running it will tell you.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, ");
System.out.println("World!");
}
}| compiles? | runs? | output | correct? | |
|---|---|---|---|---|
| this version | yes | yes | Hello, / World! on two lines | no, if one line was wanted |
| with print on line 3 | yes | yes | Hello, World! on one line | yes |
The problem is that the first line uses println when we probably meant print. The program does exactly what it was told to do — which is the defining feature of a logic error. Identifying them is hard because you have to work backwards from the output.
Trap
The assumption. No errors from javac, so the program is correct.
int total = 10;
int count = 4;
System.out.println("Average: " + total / count);| check | result |
|---|---|
| does it compile? | yes — no compile-time error |
| does it run? | yes — no exception |
| is the output right? | no — displays 'Average: 2', not 2.5 |
Two integer operands, so integer division, so the average is wrong by a quarter. Every tool in the toolchain reports success.
Compiling is a floor, not a ceiling. The tools check legality; only you check meaning.
int total = 10;
int count = 4;
System.out.println("Average: " + (double) total / count);| kind of error | who catches it |
|---|---|
| compile-time | javac, before anything runs |
| run-time | the JVM, by crashing with a message |
| logic | you, by checking the output against what you expected |
The cast (double) is Chapter 3's material; the point here is the third row of the table. Predicting the output before you run is the only defence against the third kind of error, which is why every worked example in this course ends with a verify step.
Notation
Run-time error messages are very useful for debugging. Every part of this one is telling you something.
Annotate
Exception in thread "main" — the error happened on the main thread, which for now is the only one your programs have.java.lang.ArithmeticException — the NAME of the exception. This is the category, and it is what you would search for./ by zero — the specific message, indicating more precisely what happened. The name tells you the kind, this tells you the instance.at Hello.main — the method where the error occurred: the method main in the class Hello.(Hello.java:5) — the file the method is defined in, and the line number. Unlike compile errors, this line number is where the error actually happened.Four pieces of information in two lines: what kind, what specifically, which method, which line. Getting into the habit of reading all four is what turns a crash from alarming into informative.
Hypothesis
A question about where your debugging time will actually go.
Predict first
Which of the three kinds of error is generally hardest to track down?
Correct: Logic errors, because no tool reports them and you must work backwards from the output
Why: Compile-time and run-time errors both announce themselves and name a line. A logic error announces nothing — the program does exactly what you told it to do — so you have to notice the output is wrong, reason backwards to a cause, and test the hypothesis. Think Java says it directly: the compiler and interpreter cannot help, since they do not know what the right thing is.
Constraint
Practise the backwards reasoning, on something small.
Discussion prompt
A program is supposed to print a student's average of 10 marks out of 4 tests. It prints Average: 2. It compiles and it does not crash. List the hypotheses you would test, in the order you would test them, and say how you would distinguish between them.
Hint: The output is not obviously nonsense — it is close to right, which is a clue in itself.
Answer:
First hypothesis: integer division. 10 / 4 is 2 by integer division and 2.5 exactly. The symptom — a result that is right but truncated — points straight here. Test it by printing 10.0 / 4.
Second: wrong operands. Perhaps count is not 4, or total is not 10. Test by printing both variables before the division.
Third: the wrong operator. Perhaps a - where a / was meant. Test by reading the line, which is cheap and should probably have been first.
The general method: the shape of the wrongness suggests the cause. Truncated-but-close means integer division; wildly wrong means wrong operands or operator; nothing at all means a missing println. Learning to read the symptom is most of debugging.
Comparison
Fill the blanks. This table is worth being able to reproduce from memory.
Comparison matrix
| compile-time | run-time | logic | |
|---|---|---|---|
| when it appears | when you compile | while the program is running | never — you notice the output is wrong |
| does the program run? | no, it is never built | it starts, then stops | yes, all the way through |
| do you get a message? | yes, with a line number | yes, with an exception name and a line | no message at all |
| example | a missing semicolon | dividing by zero | println where print was meant |
Read the bottom-right cell and remember that the program is doing exactly what you told it to. That is not a consolation — it is the diagnostic clue.
Pattern
Three of this lesson's four surprises have the same shape: an operator behaves according to the types of its operands, and you were looking somewhere else.
| the surprise | where you were looking | where to look |
|---|---|---|
| double y = 1 / 3 gives 0.0 | the type of y | the types of 1 and 3 |
| "n=" + 1 + 2 gives n=12 | the numbers | the leftmost operand, which is a string |
| 0.1 added ten times is not 1.0 | the arithmetic | how 0.1 is stored in binary |
| it compiled, so it works | the compiler | the output, against your prediction |
+ concatenates if either operand is a string, and evaluation is left to right.Check
Work it out before you click.
Check your understanding
What does System.out.println(7 / 2.0); display?
Answer: A
Why: One operand is a double, which is enough to make Java perform floating-point division, so the exact result 3.5 is computed and displayed. Only when both operands are integers does Java truncate toward zero.
Check
Work it out before you click.
int x = 5;
System.out.println("x + 1 = " + x + 1);| step | operands | result |
|---|---|---|
| 1 | "x + 1 = " and x | "x + 1 = 5" |
| 2 | "x + 1 = 5" and 1 | "x + 1 = 51" |
Check your understanding
What does this display?
Answer: A
Why: The leftmost + has a string on its left, so it concatenates and turns 5 into text; the result is a string, so the next + concatenates the 1 as well. To display 6 you would need "x + 1 = " + (x + 1), which forces the addition before any concatenation.
x + 1 inside a string.Check
Work it out before you click.
int count = 0;
System.out.println(100 / count);| stage | outcome |
|---|---|
| compile | succeeds — dividing by a variable is legal |
| run | throws ArithmeticException: / by zero |
| output | a stack trace, then the program stops |
Check your understanding
What kind of error is this?
Answer: A
Why: The code is legal Java, so it compiles; the problem only appears once the program is running and the interpreter tries to divide by zero, which throws an ArithmeticException and terminates the program. That is the definition of a run-time error.
Real world
Rounding error is not a textbook curiosity. It has caused documented failures with serious consequences.
Discussion prompt
You now know that 0.1 cannot be stored exactly in binary, and that errors accumulate over repeated operations. Where in a real system would that be most dangerous — and what makes such a bug so hard to catch in testing?
Hint: Think about how long it takes for a tiny error to become visible.
Answer:
The dangerous places are systems that accumulate over long periods: financial ledgers, cumulative totals, and anything counting elapsed time in small increments. A single operation is fine; a million are not.
It is hard to catch in testing precisely because short tests do not accumulate enough error to be visible. A test that adds 0.1 ten times might pass; the same code running for a month does not. The Patriot missile failure at Dhahran in 1991 was caused by exactly this shape of problem — a small timing error that grew over a hundred hours of continuous operation.
The transferable habit: when you choose a floating-point type, ask how many operations the value will go through before anyone looks at it. If the answer is 'a lot', ask whether an integer count of the smallest unit would do instead.
Commit first
Commit to an answer and to your confidence.
Predict first
What does System.out.println(1 + 2 + "3" + 4 + 5); display?
Correct: 3345
Why: Evaluation is strictly left to right. 1 + 2 has two numbers so it adds, giving 3. Then 3 + "3" has a string operand so it concatenates, giving "33". From that point everything is a string, so 4 and 5 are appended as text: "334" then "3345". The rule is that the first string turns every subsequent + into concatenation — and everything before it was ordinary addition.
Explain it
Two minutes, out loud.
Discussion prompt
Explain to a classmate why double y = 1 / 3; gives 0.0 rather than 0.333. They will object that y is clearly a double, so the division should produce a decimal. Answer that objection directly.
Hint: The order in which things happen is the whole answer.
Answer:
Java evaluates the right-hand side completely before it thinks about the variable at all. On the right there are two integers, so it does integer division and gets 0. Then it looks at y, sees a double, and converts 0 into 0.0. The type of y is real, but it arrives too late — the precision was already gone.
The objection is worth taking seriously rather than brushing aside, because it is based on a reasonable expectation: that a language would look at what you asked for. Java does not. It evaluates expressions bottom-up from their operands, and the destination has no influence on that.
Exit ticket
One question before you close the deck.
Predict first
Your program compiles cleanly, runs to completion, and prints the wrong number. Which kind of error is it, and which tool will help you find it?
Correct: A logic error, and no tool will help — you must reason backwards from the output
Why: Compiling and running successfully rules out the first two kinds entirely — those announce themselves. What is left is a logic error: the program did exactly what you told it to, which was not what you meant. The compiler and interpreter cannot help because neither knows what the right answer was, so the method is to compare the output against a prediction and work backwards to the first step where they diverge.
Connect it up
One page, from memory.
Draw it
Draw a table with three columns headed compile-time, run-time and logic. In each column write: when the error appears, whether the program runs, what message you get, and one example from this lesson. Then, underneath, write out the four expressions 1 / 3, 1.0 / 3, "n" + 1 + 2 and "n" + (1 + 2) with their values, and one sentence saying what decides each one.
Recap
Five sections: two new capabilities, one set of rules for combining them, and the classification of errors you will use for the rest of the book.
| if you remember one thing | it is this |
|---|---|
| about types | the operands decide, not the destination |
| about precision | money is counted, not measured |
| about errors | it compiled is not it works |
double holds values with decimal places, and one double operand makes a division floating-point.double y = 1 / 3; gives 0.0 — the right-hand side is evaluated before the assignment is considered.+ concatenates when either operand is a string, converting the other to text.* and /, then + and -, left to right within a level.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.