What are the performance implications of using List<Integer> instead of int[], and when does it matter?
answer
- Boxed Integer ~16 bytes vs 4 for int
- Scattered objects -> cache misses
- add boxes, get unboxes -> GC pressure
- Integer cache -128..127
- int[] / IntStream / fastutil to avoid it
basics
~20 sEach number in a List<Integer> becomes a separate object on the heap with extra memory, plus the cost of boxing and unboxing. An int[] stores the raw numbers compactly with no boxing. It matters most for large data or tight loops.
solid answer
~40 sA List<Integer> stores boxed objects: every element is an Integer on the heap (object header + the int + padding, roughly 16 bytes) plus a reference to it, versus 4 bytes per element in an int[]. So memory can be 4-5x higher, and the elements are scattered across the heap, hurting CPU cache locality. On top of that, autoboxing (Integer.valueOf) and unboxing (intValue) run on every add/get; in hot loops that adds CPU and creates garbage that pressures the GC. For small or rarely-touched collections this is irrelevant and List<Integer> is the readable choice. It matters for large datasets, hot numeric loops, or low-latency code, where you'd use int[], a primitive stream (IntStream), or a primitive-specialized collection (fastutil, Eclipse Collections). One subtlety: Integer.valueOf caches -128..127, so small boxed ints are shared and cheaper.
code
java · 15 lines// Boxing-heavy: each add allocates an Integer (outside the -128..127 cache)
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) list.add(i); // ~1M Integer objects
long sum = 0;
for (int v : list) sum += v; // unbox per element
// Boxing-free alternative:
int[] arr = new int[1_000_000];
for (int i = 0; i < arr.length; i++) arr[i] = i;
long sum2 = 0;
for (int v : arr) sum2 += v; // no boxing, contiguous, cache-friendly
// Or a primitive stream:
long sum3 = java.util.stream.IntStream.range(0, 1_000_000).asLongStream().sum();go deeper
Knows boxing adds overhead and that int[] is more efficient than List<Integer> for lots of numbers.
Can quantify the cost (extra heap object per element, boxing on add/unbox on get, GC pressure) and name int[]/IntStream alternatives.
Reasons about cache locality and the Integer cache (-128..127, the == pitfall), and chooses primitive-collection libraries when justified.
Weighs the trade-off against readability and maintenance at system scale, profiles before optimizing, and sets policy on when primitive collections are warranted.
## Background terms **Boxing / autoboxing:** wrapping a primitive (`int`) into its object form (`Integer`). The compiler inserts `Integer.valueOf(x)` automatically. **Unboxing:** the reverse, `Integer.intValue()`. **Heap object overhead:** every Java object has a *header* (mark word + class pointer, typically 12 bytes on a 64-bit JVM with compressed oops) and is padded to an 8-byte boundary. So an `Integer` (header + a 4-byte `int` = 16 bytes after padding) is far bigger than the 4 bytes the `int` itself needs. **Cache locality:** CPUs read memory in cache lines (~64 bytes). Data laid out contiguously (like an array) is read efficiently; data scattered across the heap (objects reached via pointers) causes cache misses and pointer-chasing, which is slow. ## The two layouts `int[] a = new int[n];` → one contiguous block of `n * 4` bytes. Reading `a[i]` is a direct indexed memory access. No boxing, no per-element objects. `List<Integer> list` → an internal `Object[]` of *references*. Each reference points to a separate `Integer` object somewhere on the heap. So you store: the array of references (8 bytes each with compressed oops) **plus** an `Integer` object (~16 bytes) per element. Reading `list.get(i)` returns an `Integer`, and using it as an `int` unboxes it. Net effect for a million ints: an `int[]` is ~4 MB; a `List<Integer>` is roughly ~20 MB and far more cache-hostile. ## CPU cost of boxing in loops ```java long sum = 0; for (int i = 0; i < list.size(); i++) { sum += list.get(i); // unbox each Integer -> int } ``` Every iteration follows a pointer (cache miss risk) and unboxes. If you also *build* the list (`list.add(i)`), each add boxes via `Integer.valueOf(i)`, allocating an object (unless it's in the cache range) — that's garbage the GC must later collect. In a hot path this is measurable; in a one-off small list it's noise. ## The Integer cache subtlety `Integer.valueOf` caches instances for values **-128 to 127** (the high bound is configurable via `-XX:AutoBoxCacheMax`). So boxing small ints returns shared, pre-allocated objects (cheap, no allocation), but it also means `==` on two boxed `Integer`s of value 100 is `true` while value 1000 is `false` — a classic bug. Use `.equals()` or compare unboxed `int`s. ## NPE risk A `List<Integer>` element can be `null`; unboxing `null` throws `NullPointerException`. An `int[]` element is always a real `0`-default number, never null. This is a correctness, not just performance, difference. ## When to care / what to use - **Don't care:** small collections, config-sized data, code that isn't hot. Prefer `List<Integer>` for readability and the rich `List` API. - **Do care:** large numeric datasets, tight inner loops, low-latency/high-throughput systems, memory-constrained environments. - **Alternatives:** `int[]` for fixed/known data; `IntStream`/`LongStream`/`DoubleStream` for boxing-free pipelines; primitive-collection libraries (**fastutil**, **Eclipse Collections**, **Trove**, **HPPC**) for growable primitive collections with `List`-like APIs. Bottom line: the restriction (no `List<int>`) forces boxing, and boxing trades memory + CPU + a null hazard for the convenience of the generic `List` API — fine until the numbers get big or the loop gets hot.
- Why might (a == b) return true for two Integers of value 100 but false for value 1000?Integer.valueOf caches -128..127, so value 100 returns the same cached object (reference-equal), but 1000 allocates distinct objects. Always compare with equals() or as primitives.
- How do you sum a large list of numbers without boxing overhead in a stream?Use a primitive stream like IntStream/LongStream (e.g. IntStream.range(...).sum() or mapToInt on a stream) so values stay as primitives and never get boxed.
An int[] is books packed tightly on one shelf; a List<Integer> is index cards that each point to a book stored in a random aisle — you keep walking the warehouse to fetch each one.
saying these in an interview costs you the question
- Saying List<Integer> and int[] have identical memory and speed
- Using == to compare Integer values
- Optimizing tiny collections to int[] prematurely when readability matters more
- Ignoring that null elements in List<Integer> cause unboxing NPEs