skip to content
everything.coreApi.wrappersAndBoxing

wrappers and autoboxing

int vs Integer, the integer cache, and why 127 == 127 but 128 != 128.

chapter
chapter 04.02
reading time
9 min read

every primitive has a wrapper class (int → Integer) so it can live where only objects are allowed, like a List. autoboxing is just javac quietly writing Integer.valueOf(x) and x.intValue() for you.

but a wrapper is an object: it can be null, == compares arrows instead of values, and valueOf hands out cached objects for -128..127. that's why 127 == 127 and 128 != 128.

the picture

an int is a number written straight into its slot. an Integer is that number put in a little box on the heap, and the slot holds an arrow to the box. same number, different packaging.

why bother with boxes? because a lot of java only speaks objects:

  • generics: List<int> doesn't compile, List<Integer> does. (why: generics limitations.)
  • null: a field of type Integer can say "nobody told me yet". an int can only say 0.
  • a home for the helpers: Integer.parseInt, Integer.MAX_VALUE, Character.isDigit, Long.compare.
primitivewrappercached by valueOf
booleanBooleanboth: Boolean.TRUE, Boolean.FALSE
byteByteall 256 values
shortShort-128..127
charCharacter0..127
intInteger-128..127 (upper bound is tunable)
longLong-128..127
floatFloatnothing
doubleDoublenothing

autoboxing is just method calls

here's a tiny method and what javac actually wrote into the .class file (javap -c, jdk 25):

java
Integer x = 5;
int y = x + 1;
return y;

boxing is a call to Integer.valueOf. unboxing is a call to intValue(). there's no special JVM magic, which means every rule that applies to method calls on objects applies here too, including the one where the object is null.

the integer cache

Integer.valueOf doesn't always allocate. at startup, the Integer class builds an array of 256 Integer objects, one for every value from -128 to 127, and valueOf returns one of those whenever it can. small numbers are everywhere (loop counters, flags, sizes), so sharing them saves a lot of tiny objects.

v =
127
try =
Main.java
  1. 1// both lines call Integer.valueOf(127)
  2. 2Integer a = 127;
  3. 3Integer b = 127;
a == btrue
a.equals(b)true

127 is inside the cache, so both calls return the same object. == compares arrows, and the arrows match.

stack
main
a
b
heap
Integercache[255]
127
drag v across the cached range. inside it, both variables get the same object and == is true. one step outside, valueOf allocates twice and == is false, even though the numbers are equal.

== on two objects asks "same object?", never "same value?". the cache just makes that question accidentally come out right for small numbers, which is the worst kind of bug: every test with 1, 2 and 42 passes, and production fails on order number 128.

predict the output
java
Integer a = 127, b = 127;
Integer c = 128, d = 128;
IO.println((a == b) + " " + (c == d));
predict the output
java
Long id = 127L;
IO.println(id.equals(127));
predict the output
java
Integer big = 1000;
int n = 1000;
IO.println(big == n);
one level deeper:where the cache comes from, and how to change it+

it's a private nested class, Integer.IntegerCache, with a static final Integer[] cache. on HotSpot the array even comes pre-built from the CDS archive (the class data sharing file that ships with the JDK), so startup doesn't pay for it.

the upper bound is configurable: -XX:AutoBoxCacheMax=1000 (which sets the java.lang.Integer.IntegerCache.high property) makes 128 == 128 true. the lower bound is fixed at -128. please don't use this to "fix" == bugs. it's a memory/allocation knob, and your code should be correct with the default.

null, and the unboxing NPE

since unboxing is a method call, unboxing a null is null.intValue(). the classic place this happens is a map lookup:

Stock.java
import java.util.HashMap;
import java.util.Map;
 
void main() {
    Map<String, Integer> stock = new HashMap<>();
    stock.put("apples", 3);
    IO.println("apples: " + stock.get("apples"));
    int pears = stock.get("pears");   // no pears: get returns null, then null.intValue()
    IO.println("pears: " + pears);
}
output
apples: 3
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

the line has no method call in sight, which is why this NPE confuses people. the fix is to say what "missing" means: stock.getOrDefault("pears", 0).

what boxes cost

an int is 4 bytes. an Integer is a whole object: a 12-byte header plus the 4-byte value, so 16 bytes on the heap, plus the 4-byte reference pointing at it. a Long is 24.

int[10_000_000]38.1 MB
ArrayList<Integer>190.7 MB
allocated bytes for 10 million numbers, measured on temurin 25 (compressed references on, the default). the list pays for an array of references and one object per number.

time follows memory. summing that list took about 4× longer than summing the array, because every element is a pointer to chase instead of the next 4 bytes.

the famous one is an accidentally boxed accumulator:

java
Long sum = 0L;                  // capital L
for (int i = 0; i < 100_000_000; i++) {
    sum += i;                   // unbox, add, box a new Long. every time.
}

honest numbers from jdk 25: in this exact loop, C2's escape analysis notices the Longs never leave the method and deletes them, so it allocated 0 bytes and was only 1.5× slower than long (40 ms vs 27 ms). switch that off with -XX:-DoEscapeAnalysis and the same loop allocates 2.3 GB and takes 183 ms. the JIT saved you here, but only because the box never escaped. put the values in a field or a collection and nothing can save you.

the helpers worth knowing

Helpers.java
void main() {
    IO.println(Integer.parseInt("42") + Integer.parseInt("ff", 16));   // 42 + 255
    IO.println(Integer.toBinaryString(10) + " " + Integer.toHexString(255));
    IO.println(Integer.compare(3, 7) + " " + Integer.MAX_VALUE);
    IO.println((Double.NaN == Double.NaN) + " " + Double.valueOf(Double.NaN).equals(Double.NaN));
    IO.println((0.0 == -0.0) + " " + Double.valueOf(0.0).equals(-0.0));
    try {
        Integer.parseInt("4 2");
    } catch (NumberFormatException e) {
        IO.println(e.getMessage());
    }
}
output
297
1010 ff
-1 2147483647
false true
true false
For input string: "4 2"

parseInt gives you an int, valueOf gives you an Integer (through the cache). use Integer.compare(a, b) in comparators, never a - b, which overflows (more in casting and overflow). and notice Double.equals disagrees with == on NaN and on -0.0: equals compares bit patterns, so a NaN can be found in a HashSet<Double>.

keylines
  • autoboxing is valueOf() on the way in and xxxValue() on the way out. nothing more.
  • wrapper == wrapper compares objects. use equals, or compare primitives.
  • valueOf caches -128..127, so == works on small numbers and lies on big ones.
  • unboxing null throws an NPE, often on a line with no visible method call.
  • a box costs 16 bytes or more. in hot code and big collections, stay primitive.