What are the performance costs of autoboxing, and how do you avoid accidental boxing in hot code?
answer
- Box = heap allocation; unbox = method call
- Wrapper accumulator in a loop = garbage storm
- List<Integer> chases pointers vs packed int[]
- Use IntStream/LongStream + mapToInt, not Stream<Integer>
- fastutil/Eclipse Collections for primitive maps/lists
basics
~20 sEach 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 sBoxing 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
Knows wrappers cost more than primitives and that loops with wrappers can be slow.
Can identify the Long sum accumulator trap and prefer primitive types in loops and arrays over boxed collections.
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.
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[]`