Skip to main content

Wrapper Classes in Java: Autoboxing, the Integer Cache and the Traps to Avoid

Wrapper classes in Java explained with real output: the eight wrappers, autoboxing and unboxing, the Integer cache (127 vs 128), NullPointerException on unboxing, equals traps, NaN and negative zero.

A wrapper class is an object that holds one primitive value: Integer wraps int, Double wraps double, and so on. You need them because generics and collections work only with objects (List<int> is illegal), and because a wrapper can be null. Java converts between the two automatically, which is called autoboxing (primitive to wrapper) and unboxing (wrapper to primitive). That convenience hides several traps: == on boxed values, NullPointerException when unboxing, and equals that compares types.
PrimitiveWrapperRange (from the program below)
byteByte-128 .. 127
shortShort-32768 .. 32767
intInteger-2147483648 .. 2147483647
longLong-9223372036854775808 .. 9223372036854775807
floatFloat1.4E-45 .. 3.4028235E38 (smallest positive .. largest)
doubleDouble4.9E-324 .. 1.7976931348623157E308 (smallest positive .. largest)
charCharacter0 .. 65535
booleanBooleanfalse, true
int 42 Integer.valueOf(42) boxed.intValue() Integercached -128..127 autoboxingunboxing
Tested against. Temurin 25.0.4.1. Output comes from two programs that are not in a repository: download WrapperTraps.java and download Cache.java. Compile with javac -g so null-pointer messages name the variable. The cache range for Integer is configurable, so the 127 and 128 results assume default settings.

Autoboxing is just a method call

The compiler rewrites boxing as Integer.valueOf(int) and unboxing as intValue(). There is no magic, which also means every boxing can allocate an object and every unboxing can throw.

Integer boxed = 42;     // Integer.valueOf(42)
int unboxed = boxed;    // boxed.intValue()
boxed=42 unboxed=42

Download: WrapperTraps.java.

The Integer cache: why 127 == 127 but 128 != 128

Integer.valueOf returns a shared, pre-built object for values from -128 to 127 and creates a new one otherwise. Two boxed references to a cached value are the same object; outside the range they are different objects with the same value, and == compares references.

Integer a = 127, b = 127, c = 128, d = 128;
a == b            // true
c == d            // false
c.equals(d)       // true
127 == 127 : true
128 == 128 : false   equals: true
Integer.valueOf(127) == Integer.valueOf(127): true
Long cache also -128..127: true / false
Boolean and Byte are cached too  : true true
Double is never cached : false

Source: WrapperTraps.java. Long, Short and Character follow similar caching rules, Boolean and Byte are always cached, and Double and Float never are. The rule for comparing boxed values is always the same: use equals (or unbox to a primitive), never ==.

The upper bound is tunable. Run Cache.java twice:

$ javac -g wrapper/Cache.java && java  Cache     (25.0.4.1)
1000 == 1000 : false
$ javac -g wrapper/Cache.java && java -XX:AutoBoxCacheMax=2000 Cache     (25.0.4.1)
1000 == 1000 : true

The flag changes the answer, which is exactly why code that depends on == for boxed values is a bug even when it appears to work. Download: Cache.java.

Going deeper: wrappers and Project Valhalla

Boxing costs an allocation and a pointer hop, and a List<Integer> is a list of references. JEP 401 proposes making Integer and the other wrappers value classes, which would let the JVM flatten and inline them and would change what == means for them. It is a preview feature proposal, not something to code against; read the value classes guide.

Unboxing null throws NullPointerException

A wrapper can be null; a primitive cannot. Assigning a null wrapper to a primitive, or using it in arithmetic, calls intValue() on null.

Map<String, Integer> stock = new HashMap<>();
int n = stock.get("pens");                          // NPE
Integer r = flag ? stock.get("pens") : 0;           // NPE too
Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
ternary: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

Source: WrapperTraps.java. The second case is the sneaky one: in flag ? Integer : int the ternary’s type becomes int, so the Integer branch is unboxed even though you assign the result to an Integer. The fix is stock.getOrDefault("pens", 0) or an explicit null check. Both are map lookups, as covered in how HashMap works; for the same trap with Optional see the Optional guide.

equals() checks the type, == can unbox

Each wrapper’s equals returns false for any other wrapper type. An int literal boxes to Integer, so it never equals a Long. But == between a wrapper and a primitive unboxes the wrapper and compares numbers.

Long.valueOf(5).equals(5)  : false   (5 boxes to Integer)
Long.valueOf(5).equals(5L) : true
Integer 5 == long 5L (unboxes): true

Source: WrapperTraps.java. This shows up in practice as a Map<Long, String> lookup with map.get(5): the key is an Integer, so the lookup misses and returns null with no error.

Double: NaN and negative zero

Primitive == follows the IEEE 754 rules, while Double.equals compares the bit patterns so that it stays consistent with hashing. The two disagree in exactly two cases.

NaN == NaN                   : false
Double.valueOf(NaN).equals(NaN): true
0.0 == -0.0                  : true
Double.valueOf(0.0).equals(-0.0): false

Source: WrapperTraps.java. Consequences: a NaN can be found in a HashSet<Double> although NaN != NaN, and 0.0 and -0.0 count as two different keys. Use Double.compare to get a total order.

Parsing and the useful static methods

parseInt("ff", 16) = 255, toBinaryString(10) = 1010, MAX+1 = -2147483648
parseInt("12a"): For input string: "12a"
Integer.compare(3, 7) = -1, Integer.sum(3, 7) = 10
Character.isDigit('7')=true getNumericValue('z')=35

Source: WrapperTraps.java. Notes: Integer.MAX_VALUE + 1 silently wraps to the minimum; use Math.addExact to fail loudly. parseInt throws NumberFormatException on bad input, so validate or catch it. Integer.compare beats subtraction for comparators because a - b can overflow; see Comparable vs Comparator. Wrapper values are also immutable and safe as map keys; see immutable collections and generics for why collections need them.

Boxing in loops and streams

A loop that sums into a boxed Long allocates a new object on every iteration. Use a primitive accumulator, or the primitive streams (IntStream, LongStream) that avoid boxing entirely; the details are in the Streams guide.

Common mistakes

Comparing boxed values with ==

It works for -128..127 and fails above, as the 127 and 128 output shows. Use equals.

Unboxing a value that may be null

Map lookups and database columns are the usual sources. Use getOrDefault or check for null first.

Mixing Integer and Long keys

equals is type strict, so map.get(5) never finds a Long key 5. Use 5L.

Using new Integer(…)

The constructors are deprecated for removal. Use Integer.valueOf or autoboxing.

Boxed accumulators in hot loops

Each addition allocates. Prefer primitives inside the loop.

Common interview questions

Why does Integer 127 == 127 print true but 128 == 128 print false?

Integer.valueOf reuses cached objects for -128 to 127, so both references point to one object. 128 creates two objects and == compares references. Use equals.

What is autoboxing and how does the compiler do it?

Automatic conversion between a primitive and its wrapper. The compiler inserts Integer.valueOf(int) for boxing and intValue() for unboxing.

Why can’t generics use primitives?

Type arguments must be reference types because generics are erased to Object. So you use List<Integer>, paying for boxing. See the generics guide.

When does autoboxing throw a NullPointerException?

When a null wrapper is unboxed: assigned to a primitive, used in arithmetic, or pulled through a ternary that unifies to a primitive type.

Are wrapper classes immutable?

Yes. Every operation yields a new object, which makes them safe as map keys and between threads. More in the string programs interview post for the same idea applied to String.

FAQ

What are the eight wrapper classes in Java?

Byte, Short, Integer, Long, Float, Double, Character and Boolean.

What is the range of the Integer cache?

-128 to 127 by default; the upper bound can be raised with -XX:AutoBoxCacheMax, as the Cache.java run shows.

Should I use int or Integer?

Use int by default. Use Integer when you need null, a generic type argument, or a collection element.

The rule of thumb. Use primitives for local arithmetic, wrappers where an object is required, and equals (never ==) to compare wrappers. Related: enums, lambda expressions and collections interview questions.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.