Definition
Three concepts work together:
- Autoboxing. The compiler automatically converts
inttoIntegerby insertingInteger.valueOf(value). This happens whenever a primitive appears where an object is expected. - Unboxing. The compiler automatically converts
Integertointby insertingwrapper.intValue(). This happens whenever a wrapper appears where a primitive is expected. - Integer cache.
Integer.valueOf()returns a pre-created shared object for values -128 to 127. Two autoboxed Integers in this range point to the same object, so==returns true. Outside this range, each call creates a new object, so==returns false even if the values match.
A primitive is a bare number on a scrap slip. A wrapper is that number placed inside a sealed envelope with a tracking label. Autoboxing is the clerk sealing the number into an envelope whenever the system demands an envelope. Unboxing is tearing the envelope open to read the number. The bank keeps a tray of pre-sealed envelopes for small amounts (-128 to 127). If two people request envelope for amount 100, they get the same pre-sealed envelope from the tray. For amount 1000, two fresh envelopes are printed, so they are different envelopes even though the amount inside is the same.
Understanding the Concepts
What is autoboxing?
Autoboxing is the automatic conversion from a primitive to its wrapper class. The compiler inserts it whenever a primitive appears where an object is needed.
// You write:
Integer count = 5;
// Compiler generates:
Integer count = Integer.valueOf(5);
The most common place this happens is with collections, because generics only accept objects:
List<Integer> transactionAmounts = new ArrayList<>();
transactionAmounts.add(50000); // autoboxes int 50000 to Integer
transactionAmounts.add(12000); // autoboxes int 12000 to Integer
Each wrapper class has its own valueOf method: Integer.valueOf(), Long.valueOf(), Double.valueOf(), Boolean.valueOf(), etc.
What is unboxing?
Unboxing is the reverse: converting a wrapper object back to a primitive. The compiler inserts it whenever a wrapper appears where a primitive is expected.
Integer wrapped = Integer.valueOf(42);
// You write:
int value = wrapped;
// Compiler generates:
int value = wrapped.intValue();
Unboxing also happens in arithmetic expressions:
Integer a = 100;
Integer b = 200;
int sum = a + b; // unboxes both, then adds as primitives
Why does autoboxing in loops cause performance problems?
Every autoboxing call creates a wrapper object on the heap. In a tight loop, this creates millions of short-lived objects that immediately become garbage.
// BAD: Integer accumulator, boxes every iteration
Integer total = 0;
for (int i = 0; i < 5_000_000; i++) {
total += amounts[i]; // unbox → add → box → new Integer object
}
// Creates ~5 million Integer objects → heavy GC pressure
// GOOD: primitive accumulator, zero allocation
long total = 0;
for (int i = 0; i < 5_000_000; i++) {
total += amounts[i]; // pure primitive arithmetic
}
// Creates 0 objects → fast
In a banking batch job processing millions of transactions, this difference can be 10x or more in execution time.
How does the Integer cache work?
Integer.valueOf(int) has a cache. For values -128 to 127, it returns a pre-created shared object from an internal array. For values outside this range, it creates a new Integer object every time.
// Inside the cache range: same object
Integer a = 100; // Integer.valueOf(100) → cached object
Integer b = 100; // Integer.valueOf(100) → SAME cached object
System.out.println(a == b); // true (same reference)
System.out.println(a.equals(b)); // true (same value)
// Outside the cache range: different objects
Integer x = 1000; // Integer.valueOf(1000) → new object
Integer y = 1000; // Integer.valueOf(1000) → new object (different)
System.out.println(x == y); // false (different references!)
System.out.println(x.equals(y)); // true (same value)
This is why you must always use .equals() to compare wrappers, never ==.
Which wrappers have caches?
| Wrapper | Cached range | Notes |
|---|---|---|
| Integer | -128 to 127 | Upper bound tunable with -XX:AutoBoxCacheMax |
| Long | -128 to 127 | Fixed range |
| Short | -128 to 127 | Fixed range |
| Byte | -128 to 127 | Entire byte range (always cached) |
| Character | 0 to 127 | ASCII range |
| Boolean | TRUE and FALSE | Always cached (only two values) |
| Float | None | Never cached |
| Double | None | Never cached |
// Boolean is always cached
Boolean a = true;
Boolean b = true;
System.out.println(a == b); // true (always same Boolean.TRUE)
// Double is never cached
Double x = 3.14;
Double y = 3.14;
System.out.println(x == y); // false (always new objects)
System.out.println(x.equals(y)); // true
What happens when you unbox null?
Unboxing a null wrapper throws NullPointerException. This is one of the most common bugs in Java.
Map<String, Integer> pendingCounts = new HashMap<>();
pendingCounts.put("SAV2001", 3);
// Key "CUR3001" does not exist → get returns null
Integer priyaCount = pendingCounts.get("CUR3001");
// CRASH: unboxing null throws NPE
// int count = priyaCount; // NullPointerException!
// FIX 1: null check
int count = priyaCount != null ? priyaCount : 0;
// FIX 2: getOrDefault (cleaner)
int safeCount = pendingCounts.getOrDefault("CUR3001", 0);
This happens frequently when:
- A Map returns null for a missing key
- A database column is NULL and maps to an Integer field
- A method returns a wrapper that can be null
How does autoboxing interact with generics?
Java generics only work with objects, not primitives. You cannot write List. You must write List.
// Does not compile: generics require objects
// List<int> amounts = new ArrayList<>();
// Correct: use wrapper
List<Integer> amounts = new ArrayList<>();
amounts.add(50000); // autoboxes int to Integer
int first = amounts.get(0); // unboxes Integer to int
This is why autoboxing exists: to bridge the gap between primitives and generics. Every time you put a primitive into a collection, it gets boxed. Every time you read it back, it gets unboxed.
How do comparison operators work on wrappers?
The <, >, <=, >= operators unbox both operands to primitives before comparing. So they always compare by value and work correctly.
Integer a = 1000;
Integer b = 2000;
// These unbox and compare correctly:
System.out.println(a < b); // true (unboxes both, compares as int)
System.out.println(a >= b); // false
// But == does NOT unbox – it compares references:
Integer c = 1000;
System.out.println(a == c); // false (different objects!)
System.out.println(a.equals(c)); // true (correct value comparison)
So < and > are safe on wrappers, but == is not.
How do you avoid boxing with streams?
Java provides primitive-specialized streams that avoid autoboxing entirely:
import java.util.stream.IntStream;
// BAD: Stream<Integer> boxes every element
long sum1 = List.of(100, 200, 300).stream()
.mapToInt(Integer::intValue) // unboxes
.sum();
// GOOD: IntStream works with primitives, no boxing at all
long sum2 = IntStream.of(100, 200, 300)
.sum(); // pure primitive arithmetic
// GOOD: generate primitive stream from array
int[] amounts = {50000, 12000, 8000};
long total = IntStream.of(amounts).sum(); // no boxing
Primitive streams: IntStream, LongStream, DoubleStream. Use them for bulk numeric operations to avoid boxing overhead.
How do you detect autoboxing in code?
# Compile with boxing warnings
javac -Xlint:boxing MyApp.java
# Example warning:
# warning: [boxing] boxing of value type 'int'
# Integer total = 0;
# ^
- IDEs also help:
- IntelliJ IDEA highlights autoboxing with a subtle color change and offers inspections for "Auto-boxing" and "Auto-unboxing".
- SonarQube rule
java:S5411warns about unnecessary autoboxing.
What about method overloading and autoboxing?
When both a primitive and a wrapper overload exist, Java prefers the exact match:
static void process(int value) { System.out.println("primitive int"); }
static void process(Integer value) { System.out.println("wrapper Integer"); }
process(42); // "primitive int" (exact match for int literal)
process(Integer.valueOf(42)); // "wrapper Integer" (exact match for Integer)
But if only one overload exists, autoboxing/unboxing bridges the gap:
static void process(Integer value) { System.out.println("Integer: " + value); }
process(42); // autoboxes int to Integer, calls process(Integer)
What are common mistakes?
- Using == to compare wrappers. Works for small values (cache), fails for large values. Always use
.equals(). - Wrapper accumulator in loops. Creates millions of objects. Use primitives for arithmetic.
- Unboxing null. Map.get() on a missing key returns null. Unboxing it crashes. Use getOrDefault().
- Mixing == with < and > reasoning.
a < bunboxes and works.a == bdoes not unbox and compares references. - Using new Integer() instead of valueOf().
new Integer(100)always creates a new object, bypassing the cache. Deprecated since Java 9. Always useInteger.valueOf(100)or autoboxing.
What should a senior engineer know?
- The cache upper bound is tunable. Use
-XX:AutoBoxCacheMax=1000to extend the Integer cache. Some high-frequency trading systems extend it to cover all commonly used values. - new Integer() is deprecated since Java 9. It bypasses the cache and always creates a new object. Use
Integer.valueOf()or autoboxing. - Escape analysis can eliminate boxing. If the JIT detects that a boxed Integer never escapes a method, it can scalar-replace it and keep the value as a primitive in a register. This means autoboxing in a method that the JIT has compiled may have zero overhead if the object does not escape.
- Project Valhalla. Future Java aims to allow value types in generics (e.g.,
Listdirectly), which would eliminate autoboxing for collections entirely. - Wrapper.compare() for safe comparison. Use
Integer.compare(a, b)for three-way comparison in sorting. It avoids both == traps and unboxing NPE.
// Safe comparison for sorting
List<Integer> amounts = List.of(500, 100, 1000, 50);
amounts.stream()
.sorted(Integer::compare) // safe, no == or NPE
.forEach(System.out::println); // 50, 100, 500, 1000
Visual Overview
── Autoboxing / Unboxing ──
int 42 ──autobox──→ Integer.valueOf(42) → Integer object (heap)
Integer ──unbox───→ integer.intValue() → int 42 (stack/register)
── Integer cache ──
Integer.valueOf(100) → cache[-128..127] → return cache[228] (shared)
Integer.valueOf(100) → cache[-128..127] → return cache[228] (SAME object)
→ 100 == 100 : TRUE (same reference)
Integer.valueOf(1000) → outside cache → new Integer(1000) (fresh)
Integer.valueOf(1000) → outside cache → new Integer(1000) (fresh)
→ 1000 == 1000 : FALSE (different references)
→ 1000.equals(1000) : TRUE (value comparison)
── Loop cost ──
Integer total = 0; long total = 0;
total += amount; total += amount;
│ │
├ unbox → add → box ├ add (register)
├ new Integer per iter ├ 0 allocations
└ ~5M garbage objects └ fast, no GC
── Null unbox ──
map.get("missing") → null → int x = null → intValue() → NPE
map.getOrDefault("missing", 0) → 0 → safe
── Safe alternatives ──
IntStream.of(amounts).sum() → no boxing, pure primitive
Integer.compare(a, b) → safe sorting, no == or NPE
.equals() → value comparison, always correct
Key Takeaway
Autoboxing is the compiler's automatic conversion from a primitive to its wrapper object, implemented by calling valueOf(). Unboxing is the reverse, implemented by calling intValue() or the equivalent method. These conversions are inserted silently wherever a primitive meets an object boundary, most commonly when putting values into collections.
The performance trap is in loops. If your accumulator is an Integer instead of a primitive long, every iteration creates a new wrapper object on the heap. Over millions of banking transactions, that is millions of garbage objects and heavy GC pressure. The fix is to always use primitive types for arithmetic and accumulation.
The correctness trap is the Integer cache. Integer.valueOf() caches values -128 to 127, so two autoboxed Integers in that range point to the same object and == returns true. Outside that range, each is a new object and == returns false even if the values match. This is why you must always compare wrappers with .equals(), never ==. The comparison operators < and > are safe because they unbox both operands.
The null trap is unboxing. If a Map returns null for a missing key and you assign it to a primitive, the unboxing calls intValue() on null and throws NullPointerException. Guard with getOrDefault() or a null check.
One sentence to remember: use primitives for arithmetic and loops, use .equals() to compare wrappers, and guard against null before unboxing.
---