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
Integercan say "nobody told me yet". anintcan only say0. - a home for the helpers:
Integer.parseInt,Integer.MAX_VALUE,Character.isDigit,Long.compare.
| primitive | wrapper | cached by valueOf |
|---|---|---|
boolean | Boolean | both: Boolean.TRUE, Boolean.FALSE |
byte | Byte | all 256 values |
short | Short | -128..127 |
char | Character | 0..127 |
int | Integer | -128..127 (upper bound is tunable) |
long | Long | -128..127 |
float | Float | nothing |
double | Double | nothing |
autoboxing is just method calls
here's a tiny method and what javac actually wrote into the .class file (javap -c, jdk 25):
Integer x = 5;
int y = x + 1;
return y;0: iconst_5
1: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_0
5: aload_0
6: invokevirtual #13 // Method java/lang/Integer.intValue:()I
9: iconst_1
10: iadd
11: istore_1boxing 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.
- 1// both lines call Integer.valueOf(127)
- 2Integer a = 127;
- 3Integer b = 127;
127 is inside the , so both calls return the same object. == compares arrows, and the arrows match.
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.
Integer a = 127, b = 127;
Integer c = 128, d = 128;
IO.println((a == b) + " " + (c == d));Long id = 127L;
IO.println(id.equals(127));Integer big = 1000;
int n = 1000;
IO.println(big == n);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:
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);
}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.
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:
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
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());
}
}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>.
- autoboxing is
valueOf()on the way in andxxxValue()on the way out. nothing more. - wrapper
==wrapper compares objects. useequals, or compare primitives. valueOfcaches -128..127, so==works on small numbers and lies on big ones.- unboxing
nullthrows 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.