Course contents

Wrapper classes and autoboxing

What the compiler writes when it boxes and unboxes, why two Integers of 128 aren't ==, and what boxing costs in memory and speed.

FreeLesson11 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a value in a box
  2. What the compiler actually writes
  3. The Integer cache and == comparisons
  4. Unboxing null: the NullPointerException nobody sees coming
  5. Real world: what boxing costs
  6. How to say it in the interview

📦 What are wrapper classes and autoboxing?

Every primitive has a wrapper class that turns the value into an object: int → Integer, long → Long, boolean → Boolean, and so on. Collections and generics only work with objects, so List<Integer> holds Integer objects, never raw ints. Autoboxing is the compiler quietly converting between the two: list.add(42) really compiles to list.add(Integer.valueOf(42)).

For this lesson, we'll focus on what the compiler generates for boxing and unboxing, the Integer cache that makes == behave strangely, and what boxing costs in memory and CPU. This is one of the most reliable interview topics in Java: a single "what does this print?" with two Integers tells the interviewer whether you know the difference between identity and value.

Below the line (out of scope)

  • equals() and hashCode() contracts in general (Core Java module)
  • Primitive specialisations in streams (IntStream, LongStream) beyond a quick mention
  • Project Valhalla value classes (preview, not in Java 21)

The 30-second answer

Wrapper classes (Integer, Long, Double, ...) are immutable objects that hold one primitive, so primitives can go into collections and generics. Autoboxing compiles to Integer.valueOf(x) and unboxing to x.intValue(). valueOf returns cached objects for -128 to 127, so == between two boxed values is true inside that range and usually false outside it: always compare wrappers with equals(). Unboxing null throws NullPointerException, and boxing in hot loops costs an allocation and about 16 bytes per Integer, so keep primitives in hot paths.

What you’ll learn

  • The eight wrapper classes and why valueOf replaced the deprecated constructors
  • What autoboxing and unboxing compile to, and where the hidden NullPointerException comes from
  • How the Integer cache (-128 to 127, -XX:AutoBoxCacheMax) makes == unreliable
  • What a boxed value costs: 16 bytes per Integer object and an allocation per boxing in hot loops

01Picture it first: a value in a box

Think of a wrapper as a sealed envelope with a number written inside. The number (the primitive) is the thing you care about. The envelope (the object) is what lets you post it into a List or a Map, which only accept envelopes. Two envelopes can contain the same number and still be two different envelopes, and that difference is the whole story of == versus equals().

Primitive Wrapper Cached by valueOf
byte Byte all 256 values
short Short -128 to 127
int Integer -128 to 127 (upper bound configurable)
long Long -128 to 127
char Character 0 to 127
boolean Boolean TRUE and FALSE
float Float none
double Double none

All wrappers are immutable and final. Integer x = 5; x++; doesn't change an object; it unboxes, adds one and boxes a different Integer into x.

02What the compiler actually writes

Try it yourself first

Before reading on, guess which method calls javac inserts into these three lines.

Integer boxed = 42;          // boxing
int raw = boxed;             // unboxing
List<Integer> list = new ArrayList<>();
list.add(raw);               // boxing again

Run javap -c on the class and you'll see:

bipush        42
invokestatic  java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
...
invokevirtual java/lang/Integer.intValue:()I
...
invokestatic  java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
invokeinterface java/util/List.add:(Ljava/lang/Object;)Z

Notice three things:

  1. Autoboxing is pure compile-time sugar. The bytecode has ordinary method calls; the JVM doesn't know "autoboxing" exists.
  2. Boxing always goes through valueOf, never through a constructor. That's what makes the cache apply.
  3. Unboxing is a plain method call on the reference. If the reference is null, that call throws NullPointerException.

valueOf, not the constructor

Before Java 5, you wrote new Integer(42). Don't. The wrapper constructors have been deprecated since Java 9 and marked deprecated for removal since Java 16 (JEP 390), because they always allocate and bypass the cache. Use Integer.valueOf(42) (or plain autoboxing) to get a possibly cached object, and Integer.parseInt("42") when you want a primitive from a string.

Integer a = Integer.valueOf(100);    // may return a cached instance
int b = Integer.parseInt("100");     // primitive, no object at all
Integer c = new Integer(100);        // compiles with a removal warning, always a new object

03The Integer cache and == comparisons

Here's the question that shows up in nearly every Java screen.

Try it yourself first

Predict all four lines before scrolling.

Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b);       // ?
System.out.println(c == d);       // ?
System.out.println(c.equals(d));  // ?
int e = 128;
System.out.println(c == e);       // ?
true
false
true
true

Integer.valueOf keeps a static array of pre-built Integer objects for -128 to 127 (inside Integer.IntegerCache). Boxing 127 twice hands you the same object, so ==, which compares references, says true. Boxing 128 twice creates two different objects, so == says false. The last line is true because comparing an Integer with an int unboxes the Integer and compares numbers.

Fig. 1

Boxing 127 returns the same cached Integer, so == is true; boxing 128 allocates two separate objects, so == is false while equals() is true

Boxing 127 returns the same cached Integer, so == is true; boxing 128 allocates two separate objects, so == is false while equals() is true
Where candidates slip

Many candidates say "== works for small numbers". It's frequently a mistake to rely on that, because the cache range is an implementation detail with a configurable upper bound. Tests with IDs 1 to 100 pass, and production breaks the day IDs pass 127. The rule is simple: compare wrappers with equals() (or Objects.equals(a, b) if either may be null), or unbox explicitly.

The JLS only guarantees caching for -128 to 127 (plus true, false and char 0 to 127). HotSpot lets you raise the Integer upper bound with -XX:AutoBoxCacheMax=<n> (or the system property java.lang.Integer.IntegerCache.high). With -XX:AutoBoxCacheMax=2000, Integer c = 1000, d = 1000; c == d prints true. The lower bound stays at -128, and Long, Short and Character caches aren't configurable.

More == traps

Long big = 127L;
System.out.println(big.equals(127));     // false: 127 boxes to Integer, not Long
System.out.println(big.equals(127L));    // true

Double x = 1.0, y = 1.0;
System.out.println(x == y);              // false: Double is never cached

System.out.println(true ? Integer.valueOf(1) : Double.valueOf(2)); // 1.0 (!)

The last one is sneaky: in a conditional expression with an Integer and a Double operand, both are unboxed and binary numeric promotion makes the result a double. So you get 1.0, not 1.

04Unboxing null: the NullPointerException nobody sees coming

Because unboxing is a call to intValue(), any null wrapper explodes the moment it meets a primitive context.

Map<String, Integer> counts = new HashMap<>();
int n = counts.get("missing");          // NPE: get() returned null

Integer maybe = null;
int x = flag ? maybe : 0;               // NPE: the conditional unboxes maybe

Boolean enabled = config.get("feature"); // null if the key is absent
if (enabled) { ... }                    // NPE

Since Java 14, helpful NullPointerException messages make this easier to spot in logs: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null. When you see intValue(), longValue() or booleanValue() in an NPE message, think "unboxing".

The fixes are boring and reliable: counts.getOrDefault("missing", 0), Boolean.TRUE.equals(enabled), or keep the field a primitive when "absent" isn't a meaningful state.

05Real world: what boxing costs

On a 64-bit HotSpot JVM with compressed oops (the default for heaps under about 32 GB), an object has a 12-byte header, and objects are padded to 8-byte multiples:

Thing Size
int in an int[] 4 bytes
Integer object 16 bytes (12 header + 4 value)
Long / Double object 24 bytes (12 header + 8 value, padded)
Reference to it in an Object[] 4 bytes (compressed oop)

So a million values cost about 4 MB in an int[] and about 20 MB in an ArrayList<Integer> (16 MB of Integer objects plus 4 MB of references), before counting ArrayList spare capacity. A HashMap<Integer, Integer> adds a 32-byte entry per mapping on top. Those objects are also scattered across the heap, so iterating them is slower than walking a dense array.

Note

JDK 24 added experimental compact object headers (JEP 450, made a product feature in JDK 25 by JEP 519, still off by default). With -XX:+UseCompactObjectHeaders the header shrinks to 8 bytes, so a Long drops to 16 bytes. On Java 21, assume the numbers in the table.

Performance angle

The classic boxing bug is an accumulator declared as a wrapper:

Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
    sum += i;   // unbox, add, Long.valueOf(...) every iteration
}

That's up to a million short-lived Long allocations. Declaring long sum removes all of them. Escape analysis sometimes rescues code like this, but you can't count on it. The same applies to streams: prefer mapToInt(...).sum() over map(...).reduce(0, Integer::sum).

In the real world

The most common production version I see is a Map<Long, Long> used as a counter in a hot path, or a List<Integer> with millions of IDs loaded from a database. Heap dumps show millions of java.lang.Long instances, and GC pressure goes up. The fix is a primitive array, or a primitive collection library (Eclipse Collections, fastutil) when you need map semantics.

My default: use primitives for fields, locals and arithmetic; use wrappers only where an object is required (generics, collections) or where null genuinely means "no value", such as a nullable database column.

06How to say it in the interview

When I ask about autoboxing, I'm listening for three things: do you know boxing compiles to valueOf and that it's cached for -128 to 127, do you immediately say "use equals()", and do you know the two runtime costs, NullPointerException and allocation.

In the interview

"Wrappers like Integer are immutable objects around a primitive so values can live in collections and generics. Autoboxing is compiler sugar: boxing becomes Integer.valueOf and unboxing becomes intValue(). valueOf caches -128 to 127, which is why == on two boxed 127s is true and on two boxed 128s is false, so I always compare wrappers with equals. Unboxing null throws an NPE, and boxing in a hot loop allocates a 16-byte object each time, so I keep primitives in hot paths and use IntStream or primitive arrays for large data."

Here's how this might look if you sketch it on the whiteboard:

On the whiteboard

Boxing

  • Integer x = 5 → Integer.valueOf(5)
  • int y = x → x.intValue() (NPE if null)
  • cache: -128..127 (Integer high: -XX:AutoBoxCacheMax)
  • == on wrappers = identity → use equals()
  • new Integer() deprecated for removal (Java 16)
  • Integer 16 B vs int 4 B; avoid boxing in loops
Practise this topicData types & variables: interview questions & scenarios
Further reading (7)