Command Palette

Search for a command to run...

PHASE 16Advanced ~38 min· topic 3 of 4

Topic 16.3

Tricky Output Questions

In one line

Small programs whose output surprises most people: Integer caching, the String pool, char arithmetic, overload resolution, initialisation order, finally overriding return, i++ + ++i, floating point and NaN. Predict each answer, then run it; every output here was produced by a real JDK.

Think of it like this

A magic trick looks impossible until someone shows you the hidden card up the sleeve. Then it looks obvious, and you can never be fooled by it again. Each puzzle on this page is a trick with a hidden card: one precise rule of the Java language. Find the rule and the "surprising" output becomes the only possible output.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Autoboxing
The compiler automatically wrapping a primitive such as int into its object type such as Integer, and unwrapping it again (unboxing).
Integer cache
A set of ready-made Integer objects for -128 to 127 that Integer.valueOf reuses instead of creating new ones.
Constant expression
An expression made only of literals and constant variables, such as "ja" + "va", which the compiler calculates before the program runs.
Numeric promotion
Java's rule that converts smaller number types (byte, short, char) to int, or mixes int and double to double, before doing arithmetic.
Overload resolution
How the compiler picks which of several same-named methods to call, based on the declared types of the arguments.
Static initialiser
A static block or static field assignment, run once when the class is first used.
NaN
"Not a Number": the double value produced by undefined operations such as 0.0 / 0. It is not equal to anything, even itself.
Silent overflow
When an int or long result is too big, Java wraps around to the other end of the range without any error.

Step by step

01Rule 1: boxing, the cache and the conditional operator

Integer a = 127 compiles to Integer.valueOf(127). The Java Language Specification requires valueOf to return cached objects for -128 to 127, so two boxes of 127 are the same object and == is true. 128 is outside the cache, so each valueOf(128) creates a new object and == compares two different references: false. (The upper bound can be raised with -XX:AutoBoxCacheMax, which is why you must never rely on it.)

== between a box and a primitive unboxes, so Long big = 127L; big == 127 is a numeric comparison: true. But big.equals(127) boxes 127 to an **Integer**, and Long.equals returns false for anything that isn't a Long.

The conditional operator ?: has its own typing rules. If one operand is int and the other double, the result is double, so true ? 1 : 2.0 is 1.0. If one operand is Integer and the other int, the result type is int, so a null Integer operand is unboxed and throws NullPointerException, even when you assign the result back to an Integer.

Main.javawhole filejava
Integer none = null;
Integer r = flag ? none : 0;   // type of ?: is int: none.intValue() throws NPE
Integer s = flag ? none : Integer.valueOf(0);   // both Integer: no unboxing, s = null

02Rule 2: List.remove and the Short set

List<Integer> has two remove methods: remove(int index) and remove(Object o). list.remove(1) matches remove(int) exactly, so it removes the element at index 1. Overload resolution tries methods without boxing first (Rule 5), so you must write list.remove(Integer.valueOf(1)) to remove the value 1.

In Set<Short> s; s.remove(i - 1) with short i, the expression i - 1 is an **int** (promotion), which boxes to Integer. remove(Object) accepts it, finds no equal Short (an Integer never equals a Short), and removes nothing. The compiler can't help, because Collection.remove takes Object.

03Rule 3: Strings are built left to right; chars are numbers

+ is left-associative. "x" + 1 + 2 is ("x" + 1) + 2 = "x1" + 2 = "x12". 1 + 2 + "x" is (1 + 2) + "x" = "3x". Once a String appears, every following + is concatenation.

A char is an unsigned 16-bit number. In arithmetic, char is promoted to int, so 'a' + 'b' is 97 + 98 = 195, and 'a' + 2 is 99. Cast back with (char) to get a character. Compound assignment c += 1 includes a hidden cast, so it compiles on a char, while c = c + 1 doesn't ("incompatible types: possible lossy conversion from int to char").

Strings are immutable: s.toUpperCase() returns a new String, and if you ignore the result, s is unchanged. Concatenating null produces the text "null". StringBuilder does not override equals, so two builders with the same text are not equals; use contentEquals or compare toString().

Main.javawhole filejava
char x = 'x';
int i = 0;
System.out.println(true ? x : 0);   // x   : 0 is a constant that fits in char, so the type is char
System.out.println(true ? x : i);   // 120 : i is an int variable, so the type is int

04Rule 4: increments, division and overflow

Java evaluates operands left to right, and each i++ or ++i takes effect immediately. With i = 5, i++ + ++i is: left operand i++ yields 5 and sets i to 6; right operand ++i sets i to 7 and yields 7; sum 12, i ends at 7. j = j++ yields the old value 5, increments j to 6, then the assignment stores 5 back: j stays 5.

Integer / truncates toward zero (-7 / 2 is -3), and % has the sign of the dividend (-7 % 3 is -1); Math.floorMod(-7, 3) gives the mathematical 2. 24 * 60 * 60 * 1000 * 1000 is computed entirely in int and overflows *before* it is widened to long; write 24L * … so the first multiplication is already long. Math.abs(Integer.MIN_VALUE) is still negative, because +2147483648 doesn't fit in an int.

Rule 4: increments, division and overflowdiagram
Rendering diagram…

05Rule 5: floating point, NaN and negative zero

0.1 and 0.2 have no exact binary representation; their sum is the nearest double to 0.30000000000000004, which is not the nearest double to 0.3, so == is false. Compare with a tolerance, or use BigDecimal for money (Topic 1.4).

IEEE 754 defines 1.0 / 0 as Infinity and 0.0 / 0 as NaN; only integer division by zero throws ArithmeticException. NaN == NaN is false; test with Double.isNaN. 0.0 == -0.0 is true, but Double.equals compares bit patterns, so Double.valueOf(0.0).equals(-0.0) is false while Double.valueOf(NaN).equals(NaN) is true: equals must be reflexive for collections to work.

Math.round(x) is floor(x + 0.5), so Math.round(-2.5) is -2, not -3. Casting a double to int truncates toward zero, and (int) NaN is 0. (byte) 200 keeps the low 8 bits, which read as -56 in two's complement.

06Rule 6: overloads at compile time, overrides at run time

The compiler resolves overloads in three phases (JLS 15.12.2): first without boxing or varargs (identity and widening only), then with boxing, then with varargs. The first phase that finds a match wins, and within it the most specific method is chosen. So n(5) prefers n(long) (widening) over n(Integer) (boxing) over n(int...). m(null) with m(Object) and m(String) picks String, the more specific type.

Overload choice uses the declared type: Object s = "text"; m(s) calls m(Object). Overriding uses the object's class: Animal a = new Dog(); a.sound() runs Dog.sound. Fields and static methods are not polymorphic: a.name reads Animal.name, and a.kind() calls Animal.kind().

Rule 6: overloads at compile time, overrides at run timediagram
Rendering diagram…

07Rule 7: initialisation order

When a class is first used, its static initialisers run once, top to bottom, parent class first. Each new then runs, for each class from Object downward: field initialisers and instance blocks in source order, then the constructor body. A child's fields are therefore still at their default values (null, 0, false) while the parent constructor runs.

If a parent constructor calls an overridable method, the child's override runs early and sees those defaults. Static fields have the same trap: static final Tally ONE = new Tally(); static int created = 0; runs the constructor (created becomes 1), then the next line resets created to 0.

Rule 7: initialisation orderdiagram
Rendering diagram…

08Rule 8: finally runs last and can override

return x; inside try first evaluates x and saves the value, then runs finally, then returns the saved value. Changing a primitive local in finally doesn't affect what is returned; mutating an object the saved reference points to does show.

If finally itself executes return (or throw), it replaces whatever the try was doing: an earlier return value is discarded, and a pending exception is silently thrown away. javac warns with -Xlint:finally ("finally clause cannot complete normally"), and every style guide forbids it.

Try it yourself

  1. 1

    Predict, then run

    Pick one puzzle block. Copy the program, delete the output from view, and write your nine to thirteen predictions on paper with the rule you used for each. Run it. For every miss, find the walkthrough rule that explains it and write the rule next to the puzzle number.

  2. 2

    Move the cache boundary

    On your own JDK, run Puzzles 1 with java -XX:AutoBoxCacheMax=1000 Main.java. Predict line 2 first. With the cache raised, c == d for 128 prints true, proof that code relying on == for boxes changes behaviour with a JVM flag.

    terminal
    $ java -XX:AutoBoxCacheMax=1000 Main.java
    ── expected output ──
    1) true
    2) true true
    ...
  3. 3

    Fix every trap

    Rewrite each puzzle so it does what a reader would expect: equals for boxes, remove(Integer.valueOf(...)), a (short) cast in remove, 24L for the long calculation, Math.floorMod for a positive remainder, Double.compare or a tolerance for doubles, no overridable calls in constructors and no return in finally. Run again and check each line now reads as you'd expect.

Code & diagrams

Puzzles 1: boxing, caching and collections Java 9+ New tab

7) removes "b" without an exception: after the removal the size is 2 and the iterator's cursor is 2, so hasNext() returns false and the modification check in next() never runs. 8) Arrays.asList(int[]) is a List<int[]> holding one array, because generics can't hold primitives.

Sign in to run this example in your browser.

Expected output

1) true
2) false true
3) false true
4) 1.0
5) [30]
6) 5
7) [a, c]
8) 1
9) NullPointerException
Puzzles 2: strings and chars New tab

7) constPart is a final variable initialised with a constant, so constPart + "va" is a constant expression folded to the pooled "java". part is not final, so part + "va" builds a new String at run time.

Sign in to run this example in your browser.

Expected output

1) 12
2) 3=sum
3) 195
4) ab
5) b c 99
6) hello null!
7) true false true
8) false true
9) x 120
Puzzles 3: arithmetic and floating point New tab

13) (float) 0.1 prints as 0.1, but when compared with the double 0.1 it is widened to 0.10000000149011612, a different number.

Sign in to run this example in your browser.

Expected output

1) 12 i=7
2) 5
3) 3 -3 -1 2
4) 0.30000000000000004 false
5) Infinity -Infinity NaN
6) false true true
7) true false
8) -2147483648 -2147483648
9) -56 3 -3 0
10) 3 -2 -3
11) 500654080 86400000000
12) / by zero
13) 0.1 false
Puzzles 4: overloading, overriding and hiding New tab

Only 5) is decided at run time. Everything else is fixed by the compiler from declared types: overloads (1-4, 8), field access (6) and static methods (7, which IDEs flag because a static method should be called on the class).

Sign in to run this example in your browser.

Expected output

1) m(String)
2) m(Object)
3) n(long)
4) p(double)
5) woof
6) animal dog
7) Animal.kind
8) greet(Animal)
Puzzles 5: initialisation order New tab

Static blocks run once, on first use of the class, not when main starts. Main itself has no static block, so Parent's appears only when new Child() needs it.

Sign in to run this example in your browser.

Expected output

main starts
1 Parent static block
2 Child static block
3 Parent instance block
4 Parent constructor
5 Child describe: status=null, size=0
6 Child instance block, status=ready
7 Child constructor
-- second object --
3 Parent instance block
4 Parent constructor
5 Child describe: status=null, size=0
6 Child instance block, status=ready
7 Child constructor
Tally.created = 0
Puzzles 6: finally and exceptions New tab

6) Arrays are covariant: a String[] can be referenced as Object[], so the compiler allows storing an Integer, and the JVM checks every array store at run time. Generic lists are invariant, so the same mistake with List<Object> = new ArrayList<String>() doesn't compile (Topic 8.5).

Sign in to run this example in your browser.

Expected output

1) 2
2) 1
3) try+finally
4) exception lost
5) inner-finally caught-inner outer-finally
6) java.lang.ArrayStoreException: java.lang.Integer

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Assign int arithmetic to a char

Replace c += 1; in Puzzles 2 with c = c + 1;.

terminal
$ javac Main.java
── what you'll see ──
Main.java:9: error: incompatible types: possible lossy conversion from int to char
c = c + 1;
^
1 error

Break #2

Overloads that are equally specific

Add static void m(Integer i) next to m(Object) and m(String) in Puzzles 4, then call m(null).

terminal
$ javac Main.java
── what you'll see ──
Main.java:25: error: reference to m is ambiguous
System.out.print("1) "); m(null);
^
both method m(String) in Main and method m(Integer) in Main match
1 error

Myth vs fact

Myth

== on Integer works for small numbers, so it's fine for IDs.

Fact

It works only inside the cache range, which can even be changed with a JVM flag. IDs grow past 127. Always compare boxes with equals or unbox explicitly.

Myth

i = i++ increments i.

Fact

The right side yields the old value and increments i, then the assignment stores the old value back. i is unchanged.

Myth

Dividing by zero always throws an exception.

Fact

Only integer division and remainder throw ArithmeticException. Floating-point division by zero gives Infinity, -Infinity or NaN.

Myth

finally can't change what a method returns.

Fact

A return in finally replaces the try's return value and even swallows a pending exception. Mutating a returned object in finally also shows up in the result.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Overload resolution, constant folding and the type of a conditional expression are all decided by javac and baked into the bytecode. javap -c Main shows the chosen method descriptor, for example invokestatic Main.n:(J)V for n(long), which is the fastest way to settle a puzzle argument.

  • ▸

    String concatenation since Java 9 compiles to an invokedynamic call to StringConcatFactory (JEP 280), but constant expressions are still folded at compile time and interned, so lit == "ja" + "va" remains true on every version.

  • ▸

    j = j++ compiles to: load j, increment the local variable slot, store the loaded value back into j. The increment happens and is then overwritten, which you can see in javap -c as iload, iinc, istore.

  • ▸

    Class initialisation is lazy and thread-safe (JLS 12.4.2): the JVM holds an initialisation lock per class, which is why the Singleton holder idiom (Topic 16.2) works without explicit synchronisation, and why a circular static dependency between two classes can deadlock threads that initialise them at the same time.

Remember this

  1. 1

    Interviewers use output questions to check whether you know the rules, not whether you've seen the puzzle. So for every puzzle, learn the rule it tests: boxing and caching (Topic 1.6, 9.4), String constant folding and the pool (Topic 6.1), numeric promotion of char, byte and short to int (Topics 1.5 and 1.7), evaluation order (left to right, operands before operators), overload resolution at compile time versus overriding at run time (Topics 3.4 and 5.4), initialisation order (Topic 4.6), and **finally semantics** (Topic 7.2).

  2. 2

    The method that works: evaluate exactly as the compiler and JVM do, one step at a time, writing down the *type* as well as the value of every sub-expression. Most wrong answers come from skipping a type: 'a' + 'b' is an int, true ? 1 : 2.0 is a double, and "" + 1 + 2 is a String from the first + onwards.

  3. 3

    Compile time vs run time is the second big source of puzzles. The compiler chooses overloads, folds constant expressions ("ja" + "va" becomes "java"), and decides the type of a conditional expression. The JVM chooses overridden instance methods, runs static initialisers on first use, and executes finally blocks. A field access or static method call is resolved by the declared type; an instance method call by the object's class.

  4. 4

    Floating point follows the IEEE 754 standard: double holds a binary fraction, so most decimals (0.1, 0.2) are approximations; dividing a non-zero double by zero gives Infinity, 0.0 / 0 gives NaN, and NaN is not equal to anything, including itself. Integer division truncates toward zero, % takes the sign of the left operand, and overflow wraps silently (Topics 1.3 and 1.4).

  5. 5

    Each runnable example below packs several numbered puzzles on one theme. Cover the output, write your prediction for each number, then run it. When you're wrong, the walkthrough step for that theme names the exact rule. Puzzles like these also hide in real code: the Integer == bug, the int overflow in a long calculation, and the lost exception in finally have all caused production incidents.

Explain it without notes

01

What does System.out.println("" + 'a' + 'b' + ('a' + 'b')) print, and why?

02

Why does Integer a = 1000, b = 1000; a == b print false, while int c = 1000; a == c prints true?

03

What is printed by double d = 10 / 4; System.out.println(d); and how do you get 2.5?

04

What does Math.min(-0.0, 0.0) return, and why does it differ from comparing with <?

05

A parent constructor calls an overridden method that prints a final field of the child initialised in its declaration to 42. What is printed?

Practice

01

Write a program that shows Integer == failing for a value outside the cache and fix it with equals. Print both results.

02

Write a method average(int[] values) that returns a double, and show the integer-division bug and its fix for the input {1, 2}.

03

Write a puzzle of your own about finally, predict its output, then write the program and confirm it.

Trade-offs

  • ↔

    Puzzles train precise rule knowledge, but real code should never depend on these rules: if a line needs a puzzle-solver to read it, rewrite it.

  • ↔

    Static analysis (javac -Xlint:all, IDE inspections, Error Prone, SpotBugs) catches most of these traps automatically; a team gets more safety from turning those checks on than from memorising puzzles.

Done when you can

  • Done when you can predict every line of the six puzzle programs and name the rule behind each.

  • Done when you can explain the Integer cache, the ?: typing rules and the List.remove overloads.

  • Done when you can trace i++ + ++i and j = j++ step by step.

  • Done when you can explain NaN, negative zero and why 0.1 + 0.2 != 0.3.

  • Done when you can list the initialisation order for a parent and child class, statics included.

  • Done when you can explain how finally interacts with return and exceptions.