What are autoboxing and unboxing in Java, and why can they hurt performance in hot code?
answer
- Primitive = raw value; wrapper = heap object with header
- valueOf on box, intValue on unbox
- Generics/streams can't hold primitives → silent boxing
- Cost = allocation + GC pressure + pointer indirection
- Hot path only: optimize inner loops, not config maps
basics
~20 sAutoboxing converts a primitive (like int) into its wrapper object (Integer) automatically; unboxing does the reverse. Each box can allocate a new object, which costs memory and creates garbage. In tight loops that adds up and slows things down.
solid answer
~40 sJava primitives (int, long, double, etc.) hold raw values with no object header, while wrapper types (Integer, Long, Double) are full objects on the heap. Autoboxing is the compiler automatically calling Integer.valueOf(x) when a primitive is used where an object is expected; unboxing calls intValue() for the reverse. The cost is twofold: every box may allocate a heap object (memory + initialization), and those short-lived objects raise garbage-collection pressure. In a hot loop doing millions of operations, that allocation and GC churn dominates. It also adds pointer indirection on reads. The fix is to keep primitives primitive on hot paths: use int instead of Integer, primitive arrays instead of List<Integer>, and primitive streams (IntStream) instead of Stream<Integer>.
go deeper
Define autoboxing/unboxing and name int vs Integer as primitive vs object. Knows boxing can create garbage.
Explains the allocation + GC-pressure cost, where boxing sneaks in (generics, streams, accumulators), and the primitive-stream / primitive-array fixes.
Reasons about cache locality and pointer indirection, restricts the optimization to genuine hot paths, and reaches for primitive-specialized collections when needed.
Frames it as a data-layout and allocation-rate concern across a system, weighs Valhalla's trajectory, and sets team conventions/profiling gates rather than micro-optimizing blindly.
## The two worlds: primitives vs wrappers Java has two kinds of integer-ish values: - **Primitives** — `int`, `long`, `short`, `byte`, `char`, `boolean`, `float`, `double`. These are *raw values*. An `int` is just 32 bits sitting in a register or stack slot. No object header, no heap allocation, no pointer. - **Wrapper objects** — `Integer`, `Long`, `Short`, `Byte`, `Character`, `Boolean`, `Float`, `Double`. Each is a real Java *object* living on the heap. On the HotSpot JVM an object has a header (mark word + class pointer, typically 12–16 bytes) plus the value field, so an `Integer` wrapping a 4-byte int can occupy ~16 bytes and is reached through a pointer (a *reference*). ## What is autoboxing / unboxing? **Autoboxing** is the compiler *automatically* converting a primitive to its wrapper when the context wants an object. Writing `Integer i = 5;` compiles to `Integer i = Integer.valueOf(5);`. **Unboxing** is the reverse: `int x = i;` compiles to `int x = i.intValue();`. These conversions are invisible in source code, which is exactly why they sneak in. ## Why it can be slow 1. **Allocation.** Boxing a value the cache doesn't cover allocates a fresh heap object. Allocation itself is cheap on modern JVMs (a pointer bump), but it isn't free. 2. **GC pressure.** Boxed objects are usually short-lived garbage. Create millions of them in a loop and the young-generation garbage collector runs more often, costing CPU and pauses. 3. **Indirection / cache misses.** Reading a boxed value means following a pointer to the heap, which can miss the CPU cache. A primitive array `int[]` is contiguous and cache-friendly; an `Integer[]` (or `List<Integer>`) is an array of pointers to scattered objects. 4. **Extra method calls.** Each box/unbox is a `valueOf`/`intValue` call (usually inlined by the JIT, but still conceptual overhead). ## Where it sneaks in - **Generics & collections.** Generics can't hold primitives, so `List<Integer>`, `Map<Integer, ...>`, `HashSet<Long>` box every element. `map.get(5)` boxes the `5`. - **Streams.** `Stream<Integer>` boxes; `IntStream` does not. `list.stream().mapToInt(...)` unboxes once then stays primitive. - **Ternaries and mixed expressions.** `flag ? 1 : someInteger` can box the `1`. - **Varargs of Object**, `Object`-typed fields, reflection, and logging like `log.info("{}", count)`. ## Hot path vs cold path The rule is about *hot paths* — code that runs millions of times (inner loops, per-request handlers, per-element transforms). Boxing one `Integer` to put in a config map is irrelevant. Boxing inside a loop summing a million values is not. Optimize where it matters; readable boxed code elsewhere is fine. ## The fixes - Use primitives directly (`int sum`, not `Integer sum`). - Use **primitive arrays** (`int[]`, `long[]`) instead of `List<Integer>`. - Use **primitive streams** (`IntStream`, `LongStream`, `DoubleStream`). - Use **primitive-specialized collections** (Eclipse Collections `IntList`, fastutil `IntArrayList`, Trove) when you need list/map/set semantics over primitives. - Beware accumulators: `Integer total = 0; for (...) total += x;` boxes on every iteration — make it `int total`.
- Why can't a Java generic type parameter be a primitive like int?Generics are implemented with type erasure over Object references, and primitives are not subtypes of Object. So the type argument must be a reference type — hence List<Integer>, not List<int>. (Project Valhalla aims to relax this with value/primitive classes, but it isn't standard yet.)
- Does autoboxing allocate every single time?No. Integer.valueOf caches small values (-128..127 by default), so boxing those returns a shared cached instance with no allocation. Values outside the cache allocate a new object each time.
saying these in an interview costs you the question
- Claiming autoboxing is always free because the JIT optimizes it away — allocation and GC pressure are real in hot loops
- Thinking int and Integer are interchangeable with no cost difference
- Saying List<int> is legal Java (generics require reference types)
- Optimizing boxing everywhere including cold paths, hurting readability for no gain