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.
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 = null02Rule 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().
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 int04Rule 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.
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().
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.
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
Predict, then run
Pick one puzzle block. Copy the program, delete the
outputfrom 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
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 == dfor 128 printstrue, proof that code relying on==for boxes changes behaviour with a JVM flag.terminal$ java -XX:AutoBoxCacheMax=1000 Main.java── expected output ──1) true2) true true... - 3
Fix every trap
Rewrite each puzzle so it does what a reader would expect:
equalsfor boxes,remove(Integer.valueOf(...)), a(short)cast inremove,24Lfor the long calculation,Math.floorModfor a positive remainder,Double.compareor a tolerance for doubles, no overridable calls in constructors and noreturninfinally. Run again and check each line now reads as you'd expect.
Code & diagrams
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.
Expected output
1) true
2) false true
3) false true
4) 1.0
5) [30]
6) 5
7) [a, c]
8) 1
9) NullPointerException7) 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.
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 12013) (float) 0.1 prints as 0.1, but when compared with the double 0.1 it is widened to 0.10000000149011612, a different number.
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
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).
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)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.
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 = 06) 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).
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.IntegerBreak 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;.
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).
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
javacand baked into the bytecode.javap -c Mainshows the chosen method descriptor, for exampleinvokestatic Main.n:(J)Vforn(long), which is the fastest way to settle a puzzle argument. - ▸
String concatenation since Java 9 compiles to an
invokedynamiccall toStringConcatFactory(JEP 280), but constant expressions are still folded at compile time and interned, solit == "ja" + "va"remainstrueon 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 injavap -casiload,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
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,byteandshorttoint(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 **finallysemantics** (Topic 7.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 anint,true ? 1 : 2.0is adouble, and"" + 1 + 2is aStringfrom the first+onwards. - 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 executesfinallyblocks. A field access or static method call is resolved by the declared type; an instance method call by the object's class. - 4
Floating point follows the IEEE 754 standard:
doubleholds a binary fraction, so most decimals (0.1, 0.2) are approximations; dividing a non-zero double by zero givesInfinity,0.0 / 0givesNaN, andNaNis 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
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, theintoverflow in alongcalculation, and the lost exception infinallyhave all caused production incidents.
Explain it without notes
What does System.out.println("" + 'a' + 'b' + ('a' + 'b')) print, and why?
Why does Integer a = 1000, b = 1000; a == b print false, while int c = 1000; a == c prints true?
What is printed by double d = 10 / 4; System.out.println(d); and how do you get 2.5?
What does Math.min(-0.0, 0.0) return, and why does it differ from comparing with <?
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
Write a program that shows Integer == failing for a value outside the cache and fix it with equals. Print both results.
Write a method average(int[] values) that returns a double, and show the integer-division bug and its fix for the input {1, 2}.
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.