Autoboxing and Unboxing in Java - Quiz

Total: 6 questions

1. 

What does the Java compiler turn autoboxing and unboxing into? How can you check it?

Autoboxing and unboxing (introduced in Java 5) are performed by the javac compiler, not by the JVM at run time. To box a value it inserts a call to the static valueOf() method; to unbox it calls intValue(), doubleValue(), booleanValue() or another xxxValue() method:

// What you write
Integer iOb = 100;
int i = iOb;
iOb++;
// What the compiler generates
Integer iOb = Integer.valueOf(100);
int i = iOb.intValue();
iOb = Integer.valueOf(iOb.intValue() + 1);

The conversion kicks in wherever a wrapper is expected but a primitive is given (or vice versa): assignment, method arguments and return values, arithmetic and the <, > comparisons, if/while conditions, the ternary operator, and == between a wrapper and a primitive.

You can check it with the disassembler: after javac, the command javap -c ClassName shows ordinary calls to java/lang/Integer.valueOf and java/lang/Integer.intValue in the bytecode.

2. 

Why does Short s = 4; compile while Long l = 4; does not?

The literal 4 is an int. For constants that fit the target type, the specification (JLS 5.2) allows narrowing followed by boxing into Byte, Short or Character, so Short s = 4; and Character c = 65; compile.

Widening int → long and then boxing into Long is not allowed: Long l = 4; and Double d = 1; fail with incompatible types: int cannot be converted to Long. Write Long l = 4L; and Double d = 1.0; instead.

For variables the rule is even stricter: the type must exactly match its primitive–wrapper pair. byte b = 4; Short s = b; does not compile; you need an explicit cast, Short s = (short) b;. Boxing and then widening to a supertype is fine: Object o = 4; compiles (int → Integer → Object).

3. 

Integer objects are immutable. So what happens on i++ for a variable of type Integer?

The value inside a wrapper object cannot change after creation, so i++ does not modify the old object. The compiler turns it into i = Integer.valueOf(i.intValue() + 1): the value is unboxed, incremented and boxed again, and the variable gets a reference to another object.

Other references to the old object still see the old value:

Integer y = 567;
Integer x = y;
System.out.println(y == x);      // true: the same object
y++;
System.out.println(x + " " + y); // 567 568
System.out.println(y == x);      // false: y refers to a new object

If nothing else refers to the old object, the garbage collector removes it. For values from -128 to 127 the «new» object comes from the valueOf() cache.

4. 

Which values does valueOf() cache for the different wrapper classes, and which of that does the specification guarantee?

Autoboxing calls valueOf(), which returns a pre-built object from a cache for small values:

  • Boolean — two constants, Boolean.TRUE and Boolean.FALSE;
  • Byte — all values (-128 to 127);
  • Short and Integer — -128 to 127;
  • Character — \u0000 to \u007f (0–127);
  • Long — -128 to 127, but only as a JDK implementation detail: the javadoc does not promise a cache;
  • Float and Double are not cached: for Double d1 = 1.0, d2 = 1.0;, d1 == d2 is false.

The ranges for Boolean, Byte, Short, Integer and Character are guaranteed by JLS 5.1.7. The upper bound can be raised with -XX:AutoBoxCacheMax, but only for Integer, so code that relies on == for wrappers may behave differently on different servers. The deprecated new Integer(...) constructor bypasses the cache.

5. 

In which non-obvious places does unboxing null throw a NullPointerException, and how do you unbox a value safely?

Unboxing is a hidden call to intValue() or another xxxValue(), so with null it throws an NPE even when the line contains no visible method call:

  • assigning to a primitive the result of a method that returned null: int discount = findDiscount();;
  • a Boolean condition: Boolean enabled = null; if (enabled) { ... };
  • arithmetic and comparison with a primitive: count++, count > 0;
  • a ternary operator with mixed types: in useCache ? cached : -1 the expression type is int, so cached is unboxed even when the result is assigned to an Integer.

Since Java 14 the NPE message tells you what was being unboxed: Cannot invoke "java.lang.Integer.intValue()" because ... is null.

Safe options: supply a default before unboxing — Objects.requireNonNullElse(count, 0), map.getOrDefault(key, 0), Optional.ofNullable(count).orElse(0); in a ternary operator make both operands Integer; and where a number is always expected, declare the variable as int.

6. 

Why does autoboxing in a loop hurt performance, and how do you avoid it?

Every boxing of a value outside the cache allocates a new object on the heap: on 64-bit HotSpot an Integer takes about 16 bytes versus 4 bytes for an int, and the extra objects add work for the garbage collector. A typical mistake is a wrapper accumulator:

Integer boxedSum = 0;
for (int i = 0; i < 10_000; i++) {
    boxedSum += i; // boxedSum = Integer.valueOf(boxedSum.intValue() + i)
}

The result is the same as with int sum, but the loop creates 9,984 Integer objects: the sum leaves the cache range as early as the 17th iteration. The JIT sometimes removes such allocations (escape analysis), but you cannot rely on it. The same happens with Stream<Integer> and reduce(0, Integer::sum).

How to avoid it: declare counters and sums as primitives (int, long, double); for large sets of numbers use int[] and IntStream/LongStream (for example, mapToInt(Integer::intValue).sum()); don't mix wrappers and primitives in one expression without need; keep wrappers for collections, generics and null as «no data».

Page 1 of 1