skip to content

What are the performance costs of autoboxing, and how do you avoid accidental boxing in hot code?

level: seniorimportance: should knowfreq 64%

answer

  1. Box = heap allocation; unbox = method call
  2. Wrapper accumulator in a loop = garbage storm
  3. List<Integer> chases pointers vs packed int[]
  4. Use IntStream/LongStream + mapToInt, not Stream<Integer>
  5. fastutil/Eclipse Collections for primitive maps/lists

basics

~20 s

Each autobox creates (or fetches) a wrapper object and each unbox is a method call. In loops or large sums this means many heap allocations, more garbage collection, and cache-unfriendly pointer chasing. Use primitives, primitive arrays, and primitive streams (IntStream) instead of wrappers.

solid answer

~50 s

Boxing turns a cheap stack value into a heap object: `Integer.valueOf` allocates outside the cached -128..127 range, so a loop that boxes per iteration creates garbage and pressures the GC; unboxing adds a method call each time. The infamous example is accumulating into an `Integer`/`Long` sum: `Long sum = 0L; for (...) sum += x;` reboxes a new Long every iteration — orders of magnitude slower than a primitive `long`. Collections and generics force boxing because they only hold objects, so `List<Integer>`/`Map<Integer,...>` store boxed values and chase pointers, hurting cache locality versus an `int[]`. Mitigations: use primitives and primitive arrays in hot paths; use primitive streams (`IntStream`/`LongStream`/`DoubleStream`) and their `sum()`/`average()` instead of `Stream<Integer>`; use primitive-specialized collections (e.g. Eclipse Collections, fastutil) when maps/lists of numbers dominate; and watch for accidental boxing from generic method signatures, varargs of `Object`, and `String.format`.

go deeper

for a junior

Knows wrappers cost more than primitives and that loops with wrappers can be slow.

for a middle

Can identify the Long sum accumulator trap and prefer primitive types in loops and arrays over boxed collections.

for a senior

Reasons about allocation/GC and cache locality, reaches for primitive streams and primitive-collection libraries, and knows the JIT may but won't reliably elide boxing.

for a principal

Decides where boxing matters using profiling, weighs library choices (fastutil etc.) and API design against readability, and codifies hot-path guidance for the team.

## Why boxing costs anything A **primitive** lives in a register or on the stack — reading/writing it is essentially free, and an `int[]` is a contiguous block of 4-byte values that the CPU cache loves. A **wrapper** is a heap object: it has an object header plus the value inside, lives somewhere on the heap, and is reached through a pointer. So **autoboxing** (`Integer.valueOf`) either returns a cached object (for -128..127) or **allocates a new object on the heap**; **unboxing** (`intValue()`) is a method call dereferencing that pointer. Two costs follow: 1. **Allocation + GC pressure** — every box outside the cache is a new short-lived object. In a loop this produces a stream of garbage the collector must reclaim. 2. **Indirection / poor cache locality** — a `List<Integer>` is an array of *pointers* to scattered heap objects; iterating it chases pointers, unlike a packed `int[]`. ## The textbook trap: accidental boxing in a sum ```java Long sum = 0L; // wrapper! for (long i = 0; i < 1_000_000_000L; i++) { sum += i; // unbox sum, add, RE-BOX into a new Long } ``` Every iteration unboxes `sum`, adds, and **boxes the result into a brand-new `Long`** (Longs above 127 are never cached). That is a billion allocations. Changing the declaration to `long sum = 0L;` removes all boxing and runs orders of magnitude faster. The bug is a single character (`Long` vs `long`) and is invisible at the call site. ## Collections and generics force boxing Generics in Java only range over reference types, so you cannot have `List<int>`. Numeric data in `List<Integer>`, `Map<Integer,Integer>`, `Set<Long>` is always boxed. For large numeric datasets this means more memory (object headers per element) and pointer-chasing iteration. Alternatives: - Plain **primitive arrays** (`int[]`, `long[]`) when the shape allows. - **Primitive-specialized collection libraries** (fastutil, Eclipse Collections, Koloboke) offering `IntArrayList`, `IntIntHashMap`, etc., that store primitives directly. ## Streams: use the primitive specializations `Stream<Integer>` boxes every element. The JDK provides `IntStream`, `LongStream`, `DoubleStream` that carry primitives end-to-end: ```java // boxes every element: int total = list.stream().mapToInt(Integer::intValue).sum(); // fine: mapToInt -> IntStream // avoid: list.stream().reduce(0, Integer::sum) keeps boxing int s = IntStream.rangeClosed(1, n).sum(); // no boxing ``` Use `mapToInt`/`mapToLong`/`mapToDouble` to drop into a primitive stream early, and their built-in `sum()/average()/summaryStatistics()`. ## Other sources of accidental boxing - **Generic method type parameters**: a method `<T> T pick(T a, T b)` called with ints boxes them. - **Varargs of `Object`**: `String.format("%d", x)`, `Arrays.asList(1,2,3)` box. - **Ternary with mixed numeric types** can box/unbox unexpectedly. - **Conditional logging / collection keys** computed with wrappers. ## How to find and fix - Read hot loops for wrapper-typed accumulators and collections. - Profile allocations (async-profiler, JFR allocation profiling) — a flood of `Integer`/`Long` allocations is the tell. - Replace with primitives, primitive arrays, primitive streams, or primitive-collection libraries. - Note this is a *hot-path* concern: for ordinary code, boxing's clarity is worth it; optimize where measurement shows it matters. ## Takeaway Boxing trades a free stack value for a heap object + method call. Sprinkled through normal code it's harmless; in tight loops, big collections, or large reductions it dominates via allocation, GC, and cache misses — so keep numeric hot paths on primitives.

  • Does the JIT ever remove boxing?
    Sometimes — escape analysis and scalar replacement can eliminate short-lived boxes that don't escape. But you can't rely on it (it fails across method boundaries, megamorphic calls, and many loop shapes), so don't write boxing-heavy hot code expecting it to be erased.
  • Why is `Stream<Integer>` slower than `IntStream` for a sum?
    `Stream<Integer>` carries boxed objects through the pipeline, allocating and unboxing per element; `IntStream` carries raw ints and sums them with no boxing, so it allocates far less and iterates with better cache behavior.

saying these in an interview costs you the question

  • Claiming the JIT always eliminates boxing so it never matters
  • Optimizing boxing everywhere instead of only measured hot paths
  • Using `Long sum` accumulators in loops
  • Believing `List<Integer>` has the same memory layout as `int[]`

context