Every Java primitive has a corresponding wrapper class in java.lang:

PrimitiveWrapperCache range
byteByte−128 to 127
shortShort−128 to 127
intInteger−128 to 127 (upper bound configurable)
longLong−128 to 127
floatFloatnone
doubleDoublenone
charCharacter0 to 127
booleanBooleanTRUE and FALSE constants

Autoboxing is the automatic conversion of a primitive to its wrapper when the code requires an object. Unboxing is the reverse — converting a wrapper back to a primitive.

// Without autoboxing (pre-Java 5) Integer x = Integer.valueOf(42); // Manual boxing int y = x.intValue(); // Manual unboxing // With autoboxing (Java 5+) Integer a = 42; // Autoboxing — compiler inserts Integer.valueOf(42) int b = a; // Unboxing — compiler inserts a.intValue() // Autoboxing in a collection List<Integer> numbers = new ArrayList<>(); numbers.add(1); // int 1 autoboxed to Integer.valueOf(1) numbers.add(2); int sum = numbers.get(0) + numbers.get(1); // Both unboxed to int before addition System.out.println(sum); // 3

Autoboxing also happens in method calls, variable assignments, conditional expressions, and arithmetic operations involving a mix of primitives and wrappers.

Autoboxing is entirely a compiler transformation. The JVM has no notion of autoboxing; it sees only explicit method calls in the bytecode.

// Source code Integer i = 100; int j = i + 50; // What the compiler generates (conceptually) Integer i = Integer.valueOf(100); int j = i.intValue() + 50;

This means every autoboxing call may allocate a new heap object (outside the cache range), and every unboxing call invokes a virtual method. For most code this is negligible, but it becomes significant in hot loops (see the performance section).

// Tracing all autobox/unbox events explicitly public class AutoboxTrace { public static void main(String[] args) { // 1. Assignment boxing Integer a = 200; // Integer.valueOf(200) — new object (outside cache) // 2. Method parameter boxing printDouble(5); // int 5 autoboxed to Integer.valueOf(5) // 3. Arithmetic triggers unboxing Integer x = 10; Integer y = 20; int sum = x + y; // x.intValue() + y.intValue() System.out.println(sum); // 30 // 4. Compound assignment — both box AND unbox Integer count = 0; count++; // Equivalent to: count = Integer.valueOf(count.intValue() + 1) System.out.println(count); // 1 // 5. Ternary operator forces boxing if types differ boolean condition = true; Integer result = condition ? 42 : null; // Integer (could be null) System.out.println(result); } static void printDouble(Integer n) { System.out.println(n * 2); } }

Step 4 (the ++ on an Integer) is particularly surprising: it unboxes, increments, then re-boxes — creating a new Integer object for every increment. The reference is reassigned, not the object mutated (wrapper types are immutable).

Integer.valueOf(n) does not always allocate a new object. The JVM maintains a cache of Integer objects for values from −128 to 127 (inclusive). For any value in this range, valueOf returns the same cached instance every time.

// Inside java.lang.Integer (simplified) private static class IntegerCache { static final Integer[] cache; static { // Cache values -128 to 127 (high can be configured via JVM flag) int high = 127; cache = new Integer[(high - (-128)) + 1]; int j = -128; for (int k = 0; k < cache.length; k++) { cache[k] = new Integer(j++); } } } public static Integer valueOf(int i) { if (i >= -128 && i <= Integer.IntegerCache.high) { return IntegerCache.cache[i + 128]; } return new Integer(i); }

The consequences of the cache are visible when comparing Integer objects with ==:

public class CacheDemo { public static void main(String[] args) { // Values within the cache range — same object returned Integer a = 100; Integer b = 100; System.out.println(a == b); // true (cached — same reference) System.out.println(a.equals(b)); // true // Values outside the cache range — new object each time Integer c = 200; Integer d = 200; System.out.println(c == d); // false (not cached — different references) System.out.println(c.equals(d)); // true (compares values) // Boundary values Integer e = 127; Integer f = 127; System.out.println(e == f); // true — last cached value Integer g = 128; Integer h = 128; System.out.println(g == h); // false — first uncached value Integer i = -128; Integer j = -128; System.out.println(i == j); // true — first cached value Integer k = -129; Integer l = -129; System.out.println(k == l); // false — outside cache } }

This is one of the most common Java interview questions and a real source of production bugs. Always use .equals() to compare the values of wrapper objects. Use == only to test reference identity, which for wrappers is almost never what you want.

public class EqualsPitfall { public static void main(String[] args) { // ─── Integer ─────────────────────────────────────────────────────── Integer x = 1000; Integer y = 1000; System.out.println(x == y); // false — different objects System.out.println(x.equals(y)); // true — same value // This looks like it should always work — but it depends on the cache! Integer a = 50; Integer b = 50; System.out.println(a == b); // true TODAY (cached), but never rely on this // ─── Long ────────────────────────────────────────────────────────── Long m = 200L; Long n = 200L; System.out.println(m == n); // false System.out.println(m.equals(n)); // true // ─── Mixed type comparison ───────────────────────────────────────── Integer p = 100; Long q = 100L; // p == q — compiler error: incompatible types Integer and Long System.out.println(p.equals(q)); // false! Different types System.out.println(p.longValue() == q); // true — unbox both to long // ─── The safe pattern: always use equals for wrappers ───────────── Integer score1 = Integer.valueOf(args.length > 0 ? Integer.parseInt(args[0]) : 999); Integer score2 = 999; // Never do: if (score1 == score2) ... if (score1.equals(score2)) { System.out.println("Scores match"); } // ─── Comparing nullable wrappers safely ─────────────────────────── Integer nullable = null; // Objects.equals handles null safely System.out.println(java.util.Objects.equals(nullable, 42)); // false System.out.println(java.util.Objects.equals(nullable, null)); // true } } Never use == to compare Integer, Long, Double, or any other wrapper type values. The behaviour depends on the Integer cache and can silently change if values shift across the cache boundary (−128–127). Always use .equals() or, for nullable wrappers, Objects.equals().

The same trap exists for Boolean — there are only two cached instances (Boolean.TRUE and Boolean.FALSE), so == happens to work, but relying on it is still poor style and confusing to readers.

// Common real-world bug: comparing Map return values with == Map<String, Integer> wordCount = new HashMap<>(); wordCount.put("hello", 1000); Integer count = wordCount.get("hello"); // BUG — works for small numbers (cached), silently fails for large ones if (count == 1000) { System.out.println("Found 1000 occurrences"); // Never printed! } // CORRECT if (count != null && count.equals(1000)) { System.out.println("Found 1000 occurrences"); // Correctly printed } // Also correct — unbox explicitly if (count != null && count == 1000) { // Wait — this unboxes 'count' via ==!? // This actually WORKS because the primitive literal 1000 triggers unboxing // of 'count', making it a primitive comparison. But it risks NPE if count == null. }

Autoboxing is convenient but not free. Each boxing operation outside the cache range allocates a new object on the heap, increasing garbage collection pressure. Unboxing adds a virtual method call. These costs accumulate in hot loops.

public class PerformanceDemo { static final int N = 10_000_000; // BAD — autoboxing on every iteration: N allocations + N unboxings static long sumWithBoxing() { Long total = 0L; for (long i = 0; i < N; i++) { total += i; // long i autoboxed to Long, then unboxed for += } return total; } // GOOD — all primitives, zero allocation static long sumWithPrimitives() { long total = 0L; for (long i = 0; i < N; i++) { total += i; } return total; } // ALSO BAD — Integer in the loop variable static int countMatches(List<Integer> list, int target) { int count = 0; for (Integer n : list) { // unbox each element if (n == target) count++; // == triggers unboxing of 'n' } return count; } // BETTER — use primitive stream or manual unboxing once static int countMatchesFast(List<Integer> list, int target) { return (int) list.stream() .mapToInt(Integer::intValue) // unbox stream .filter(n -> n == target) .count(); } public static void main(String[] args) { long start, end; start = System.currentTimeMillis(); long r1 = sumWithBoxing(); end = System.currentTimeMillis(); System.out.println("Boxing: " + r1 + " in " + (end - start) + " ms"); start = System.currentTimeMillis(); long r2 = sumWithPrimitives(); end = System.currentTimeMillis(); System.out.println("Primitive: " + r2 + " in " + (end - start) + " ms"); // The primitive version is typically 5–10x faster for large N } }

Practical performance guidelines:

Wrapper objects can be null; primitives cannot. Unboxing a null wrapper throws a NullPointerException — one of the most confusing NPEs because there is no explicit dereference in the source code.

public class NullUnboxing { public static void main(String[] args) { // ─── Direct null unboxing ─────────────────────────────────────── Integer boxed = null; int primitive = boxed; // NPE — compiler inserts boxed.intValue() // java.lang.NullPointerException: Cannot unbox null value // ─── In a conditional expression ─────────────────────────────── Integer a = null; int b = (a != null) ? a : 0; // Safe — guarded System.out.println(b); // 0 // ─── NPE from a Map lookup ───────────────────────────────────── Map<String, Integer> config = new HashMap<>(); // config.get("timeout") returns null for missing keys // int timeout = config.get("timeout"); // NPE! // Safe patterns: int timeout1 = config.getOrDefault("timeout", 30); // use getOrDefault Integer raw = config.get("timeout"); int timeout2 = (raw != null) ? raw : 30; // null check first int timeout3 = java.util.Optional.ofNullable(config.get("timeout")) .orElse(30); // Optional System.out.println(timeout1); // 30 // ─── NPE in arithmetic expressions ──────────────────────────── Integer x = getCount(); // returns null // int doubled = x * 2; // NPE — unboxes null int doubled = (x == null) ? 0 : x * 2; System.out.println(doubled); // 0 // ─── NPE in switch (Java 14+ enhanced switch) ───────────────── Integer status = null; // switch (status) { ... } // NPE thrown before any case is evaluated // ─── NPE in boolean unboxing — the most surprising case ──────── Boolean flag = null; // if (flag) { ... } // NPE — unboxes null Boolean to boolean if (Boolean.TRUE.equals(flag)) { // Safe null-friendly comparison System.out.println("True"); } else { System.out.println("Not true (null or false)"); // Prints this } } static Integer getCount() { return null; // Simulates an absent value from a map/DB } } // Real-world trap: auto-unboxing in method return public class NullReturnTrap { private static final Map<String, Integer> scores = new HashMap<>(); // Return type is primitive 'int' — triggers unboxing of null map result public static int getScore(String player) { return scores.get(player); // NPE if player not in map! } // Safe version 1 — use Integer as return type public static Integer getScoreSafe1(String player) { return scores.get(player); // Caller handles null } // Safe version 2 — use getOrDefault public static int getScoreSafe2(String player) { return scores.getOrDefault(player, 0); } // Safe version 3 — Optional public static java.util.OptionalInt getScoreSafe3(String player) { Integer v = scores.get(player); return v == null ? java.util.OptionalInt.empty() : java.util.OptionalInt.of(v); } public static void main(String[] args) { scores.put("Alice", 95); // getScore("Bob") would throw NPE System.out.println(getScoreSafe2("Alice")); // 95 System.out.println(getScoreSafe2("Bob")); // 0 (default) getScoreSafe3("Alice").ifPresent(s -> System.out.println("Alice: " + s)); System.out.println(getScoreSafe3("Bob").isPresent()); // false } } Java 21 enhanced NullPointerException messages now say exactly which variable was null (e.g., "Cannot unbox the return value of 'scores.get(player)'"). This makes diagnosing null unboxing bugs much easier — but prevention through correct use of Optional and getOrDefault is always better than debugging them.