Autoboxing and Auto-Unboxing
Primitives and Their Wrappers
Java has eight primitive types. Each has a corresponding wrapper class in java.lang:
| Primitive | Wrapper |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
Wrapper objects are needed wherever an Object or generic type is required — collections, generic method arguments, reflection, etc. Primitive values cannot be stored directly in a List — you must wrap them:
List<Integer> numbers = new ArrayList<>();
numbers.add(42); // autoboxing: int → Integer
int x = numbers.get(0); // auto-unboxing: Integer → int
How Autoboxing/Unboxing Works
The compiler silently inserts calls to the wrapper’s conversion methods:
Integer boxed = Integer.valueOf(42); // manual boxing
int unboxed = boxed.intValue(); // manual unboxing
The same conversions happen implicitly in the compiled bytecode for autoboxing:
Integer boxed = 42; // compiler emits: Integer.valueOf(42)
int unboxed = boxed; // compiler emits: boxed.intValue()
The Null Trap
Auto-unboxing a null wrapper throws NullPointerException:
Integer count = null;
int x = count; // NullPointerException — compiler emits count.intValue() on null
This is a surprisingly common bug. A method returning Integer may return null to indicate “no value”, and calling code that assigns the result to int blows up. Always check for null before unboxing, or use Optional<Integer> to make the absence explicit.
The Integer Cache
Integer.valueOf(int) caches values from to (by default). Autoboxing uses valueOf(), so small integers reuse cached objects:
Integer a = 100; // Integer.valueOf(100) → cached object
Integer b = 100; // Integer.valueOf(100) → same cached object
System.out.println(a == b); // true — same object from cache
Integer x = 200; // Integer.valueOf(200) → new object (outside cache range)
Integer y = 200; // Integer.valueOf(200) → different new object
System.out.println(x == y); // false — different objects
This is a trap. == appears to work for small numbers but fails for large ones. Always use .equals() for wrapper comparison. The cache is an implementation optimisation, not a semantic guarantee.
Performance Implications
Autoboxing creates temporary wrapper objects. In tight loops, this can cause significant GC pressure:
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // unbox sum, add i, box result — 3 operations per iteration
}
Each iteration unboxes sum, adds a primitive i, and boxes the result — creating up to a million temporary Integer objects that the GC must collect. The int version is dramatically faster:
int sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // simple integer addition — no object allocation
}
In numeric-heavy code, prefer primitives and primitive arrays over boxed collections.
Wrapper Utility Methods
The wrapper classes also provide static utility methods:
int x = Integer.parseInt("42"); // string → int
String s = Integer.toString(42); // int → string
Integer i = Integer.valueOf("42"); // string → Integer (boxed)
boolean isNaN = Double.isNaN(0.0 / 0.0); // check for NaN
boolean isDigit = Character.isDigit('5'); // character classification
int cmp = Integer.compare(a, b); // safe comparison (no overflow)
For parsing, prefer Integer.parseInt(s) over Integer.valueOf(s).intValue() to avoid unnecessary boxing.
Wrapper Immutability
All wrapper objects are immutable. Once an Integer is created with a particular value, that value can never change. This is why sum += i creates a new Integer each time — the operator produces a new object rather than mutating the existing one. Immutability makes wrappers safe to share (like String), but it also contributes to the allocation overhead in loops.