What autoboxing pitfalls beyond == can the Integer cache and wrapper types introduce?
answer
- loops: wrappers box every iteration -> use primitives
- unboxing null -> NPE (maps, if(Boolean))
- == identity trap from the cache
- list.remove(int) = index vs remove(Object) = value
- Integer(1).equals(Long(1)) is false
basics
~10 sAutoboxing can silently create many objects (slow loops), throw NullPointerException when unboxing a null wrapper, and cause confusing == results. Use primitives in hot loops and equals() or Objects.equals() for comparison.
solid answer
~40 sAutoboxing hides real work and real bugs. First, a loop like Long sum = 0L; for (...) sum += x; boxes a new Long every iteration — quadratic allocation and major slowdown versus a primitive accumulator. Second, unboxing a null wrapper throws NullPointerException: Integer i = map.get(missing); int j = i; blows up, and so does if (booleanWrapper) when the Boolean is null. Third, the -128..127 cache makes == unreliable, as discussed for identity. Fourth, overload resolution can surprise you — list.remove(2) removes index 2, but list.remove(Integer.valueOf(2)) removes the value 2. The defenses: prefer primitives in performance-sensitive code, use Objects.equals / equals for value comparison, and be explicit about boxing at API boundaries that may carry null.
go deeper
Knows wrappers can be null and that unboxing null throws NPE, and that loops with wrappers are slow.
Can identify boxing in loops, the NPE-on-unbox cases, and the remove(int) vs remove(Object) overload trap.
Reasons about allocation/GC impact, ternary/conditional unboxing edge cases, cross-type wrapper equals, and sets primitive-first conventions in hot code.
Drives API design (nullable wrapper vs primitive at boundaries), profiling-guided removal of boxing, and static-analysis gates to catch these patterns across a large codebase.
## Recap: what autoboxing is **Autoboxing** is the compiler automatically converting a primitive (`int`, `long`, `boolean`, …) to its wrapper object (`Integer`, `Long`, `Boolean`, …), and **auto-unboxing** is the reverse. `Integer i = 5;` boxes; `int j = i;` unboxes. Boxing of small integral values goes through the cached **Flyweight** instances (`Integer.valueOf`, range -128..127). All of this is invisible in the source, which is exactly why it bites. ## Pitfall 1 — hidden allocations in loops Wrappers are **immutable**, so every 'update' creates a new object: ```java Long sum = 0L; for (long i = 0; i < 1_000_000; i++) sum += i; // boxes a new Long each iteration ``` Each `sum += i` unboxes `sum`, adds, then **boxes the result into a new `Long`**. That is a million throwaway objects, hammering the allocator and GC. Using a primitive `long sum` removes all boxing. Rule: in hot paths and accumulators, use primitives, not wrappers. ## Pitfall 2 — NullPointerException on unboxing A wrapper can be `null`; a primitive cannot. Unboxing `null` throws **NullPointerException**: ```java Map<String,Integer> m = new HashMap<>(); int v = m.get("absent"); // get returns null -> unboxing -> NPE ``` The same trap hides in conditionals: `Boolean flag = config.get(...); if (flag) { ... }` throws if `flag` is null, because `if` needs a primitive `boolean` and unboxes. And in ternaries mixing a wrapper and a primitive, the whole expression may be unboxed, turning a 'safe-looking' null branch into an NPE. Defenses: null-check before unboxing, use `Boolean.TRUE.equals(flag)`, or keep nullable values as wrappers and handle null explicitly. ## Pitfall 3 — the cache / == identity trap Covered in depth elsewhere: `==` on two boxed values compares object identity, which the -128..127 cache makes accidentally true for small numbers and false for large ones. Use `equals()` / `Objects.equals()`. ## Pitfall 4 — overload resolution surprises `List<Integer>` has both `remove(int index)` and `remove(Object o)`. `list.remove(2)` picks the **primitive** overload and removes the element at **index 2**; to remove the **value** 2 you must write `list.remove(Integer.valueOf(2))`. Autoboxing does *not* kick in when an exact primitive overload exists, so the wrong method is chosen silently. ## Pitfall 5 — equals across wrapper types Boxing picks the type from the declared/literal type, so `Integer.valueOf(1).equals(Long.valueOf(1))` is **false** — different classes never `equals`. Mixing wrapper types in collections or comparisons can produce 'missing' lookups. ## Summary defense - Primitives in hot loops and accumulators. - `equals()` / `Objects.equals()` for value comparison; never `==` on two wrappers. - Treat any wrapper that might be null as nullable; null-check before unboxing. - Be deliberate with overloaded `remove`/varargs and with cross-type wrapper comparisons.
- Why does List<Integer>.remove(2) not remove the value 2?Because an exact primitive overload remove(int index) exists, so the compiler binds to it (removing index 2) rather than autoboxing to remove(Object). Use remove(Integer.valueOf(2)) for the value.
- How can an if-statement throw NullPointerException with no obvious dereference?If the condition is a Boolean wrapper that is null, the if unboxes it to a primitive boolean, and unboxing null throws NPE.
saying these in an interview costs you the question
- Using a wrapper accumulator in a tight loop 'for convenience'
- Assuming map.get(...) returning null is fine to assign to an int
- Believing list.remove(2) removes the value 2
- Expecting Integer and Long with the same value to be equals()