Back to Primitive Types & Wrappers
Q4Question 4 of 11

What is autoboxing, unboxing, and the Integer cache?

Key Concepts
  • Autoboxing: compiler converts primitive to wrapper via Wrapper.valueOf() – e.g., int → Integer
  • Unboxing: compiler converts wrapper to primitive via wrapper.xxxValue() – e.g., Integer → int
  • Integer.valueOf() caches values -128 to 127 – two Integers in this range are the same object
  • Outside the cache range, == compares references (fails); always use .equals() for wrappers
  • Autoboxing in loops creates millions of garbage objects – use primitive accumulators instead
  • Unboxing a null wrapper throws NullPointerException – guard with getOrDefault or null check

Definition

Autoboxing wraps a primitive into a wrapper object via valueOf(). Unboxing extracts the primitive via intValue(). The Integer cache makes == work for -128 to 127 but fail outside that range.

Three concepts work together:

  • Autoboxing. The compiler automatically converts int to Integer by inserting Integer.valueOf(value). This happens whenever a primitive appears where an object is expected.
  • Unboxing. The compiler automatically converts Integer to int by inserting wrapper.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.

JAVA
// 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:

JAVA
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.

JAVA
Integer wrapped = Integer.valueOf(42);

// You write:
int value = wrapped;

// Compiler generates:
int value = wrapped.intValue();

Unboxing also happens in arithmetic expressions:

JAVA
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.

JAVA
// 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.

JAVA
// 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 |

JAVA
// 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.

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.

JAVA
// 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.

JAVA
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:

JAVA
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?

BASH
# 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:S5411 warns about unnecessary autoboxing.

What about method overloading and autoboxing?

When both a primitive and a wrapper overload exist, Java prefers the exact match:

JAVA
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:

JAVA
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 < b unboxes and works. a == b does 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 use Integer.valueOf(100) or autoboxing.

What should a senior engineer know?

  • The cache upper bound is tunable. Use -XX:AutoBoxCacheMax=1000 to 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., List directly), 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.
JAVA
// 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

DIAGRAM
  ── 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.

---

Share
Use ← → arrow keys to navigate