JAVA / VARIABLES, PRIMITIVES AND TYPES
Autoboxing between primitives and wrapper classes
Predict where the compiler inserts valueOf and intValue calls, so wrapper == results, null unboxing crashes and overload picks stop surprising you.
What you will learn
- Read Integer x = 5 as Integer.valueOf(5) and int y = x as x.intValue()
- Name the contexts that force unboxing: arithmetic, ==, <, switch, array index, if
- Compare wrapper values with equals and keep == for primitives or identity checks
- Guard map lookups with getOrDefault before the value reaches arithmetic
Understanding Autoboxing between primitives and wrapper classes
Autoboxing and unboxing are conversions the compiler performs, not something the JVM does for you at runtime. When you write Integer boxed = 90, javac emits a call to Integer.valueOf(90); when you write int raw = boxed, it emits boxed.intValue(). That is the whole mechanism, and it explains everything odd about it: a boxed value is an ordinary object with an address, a class and the ability to be null, while a primitive has none of those properties. Java added this in version 5 mainly because generics and collections can only hold references, so a List<Integer> could never store a bare int.
Unboxing is triggered by any context that demands a primitive: arithmetic and compound assignment, the relational operators, unary minus and increment, an if or while condition, a switch selector, an array index, and == when the other operand is a primitive. Because each of those compiles to a real method call on the reference, a null wrapper throws NullPointerException on a line where you can see no method being invoked. That is the most common way autoboxing bites: total += map.get(key) looks like plain arithmetic and is really map.get(key).intValue().
Boxing goes through Integer.valueOf, which is allowed to hand back a shared instance, and the language guarantees it does so for every int from -128 to 127. So two Integer variables holding 100 are the same object and == reports true, while two holding 1000 are separate objects and == reports false even though equals reports true. Keep the model that == on two wrappers asks whether they are the same object while equals asks whether they hold the same type and value, and remember that each box is an allocation, which is why an accumulator declared Long inside a hot loop quietly creates one object per iteration.
import java.util.ArrayList;
import java.util.List;
public class Autoboxing {
public static void main(String[] args) {
List<Integer> minutes = new ArrayList<>();
minutes.add(90); // int 90 -> Integer.valueOf(90)
minutes.add(40);
minutes.add(70);
int total = 0;
for (int m : minutes) { // each element -> intValue()
total += m;
}
Integer boxedTotal = total; // 200 -> Integer.valueOf(200)
Integer sameTotal = 200;
System.out.println("minutes = " + minutes);
System.out.println("total = " + total);
System.out.println("boxedTotal + 1 = " + (boxedTotal + 1));
System.out.println("boxedTotal == total: " + (boxedTotal == total));
System.out.println("boxedTotal == sameTotal: " + (boxedTotal == sameTotal));
System.out.println("boxedTotal.equals(sameTotal): " + boxedTotal.equals(sameTotal));
}
}Autoboxing is the compiler writing Integer.valueOf and intValue calls for you, so boxed values inherit object identity and nullability that primitives never had.
Worked examples
Widening is tried before boxing
Shows that overload resolution reaches for a primitive widening conversion first and only boxes as a later resort.
public class Overloads {
static void show(long value) { System.out.println("long " + value); }
static void show(Integer value) { System.out.println("Integer " + value); }
static void show(Object value) { System.out.println("Object " + value); }
public static void main(String[] args) {
int primitive = 7;
Integer boxed = 7;
show(primitive);
show(boxed);
show(7L);
show("seven");
}
}Example explained
Line 1show(primitive) prints long: the first resolution round allows int to long widening but forbids boxing, so show(Integer) is never considered.
Line 2show(boxed) cannot use show(long) in that first round because unboxing is also forbidden there, and Integer beats the applicable Object overload as the more specific type.
Line 3Both calls carry the value 7, so resolution is decided by the static type of the argument, not by what it holds.
Line 4Delete show(long) and show(primitive) falls through to the boxing round and prints Integer 7 instead.
Where the wrapper cache stops
Demonstrates why == on two Integers flips from true to false between 127 and 128 while equals stays stable.
public class BoxedIdentity {
public static void main(String[] args) {
for (int value : new int[] {127, 128}) {
Integer first = value;
Integer second = value;
System.out.println(value + ": == gives " + (first == second)
+ ", equals gives " + first.equals(second));
}
Integer big = 128;
System.out.println("big == 128: " + (big == 128));
Boolean yes = true;
Boolean alsoYes = true;
System.out.println("yes == alsoYes: " + (yes == alsoYes));
}
}Example explained
Line 1At 127 both boxes come from the Integer.valueOf cache, so the two references point at one object and == is accidentally right.
Line 2At 128 valueOf allocates a fresh Integer each time, so == compares two different addresses while equals still compares the wrapped int values.
Line 3big == 128 is true because the primitive operand forces big to be unboxed; == means reference comparison only when both sides are wrappers.
Line 4Boolean.valueOf always returns the two shared instances, so wrapper == never fails for booleans, which is exactly what makes the Integer behaviour feel inconsistent.
A null from a map reaches arithmetic
Shows the hidden intValue() call that turns a missing map key into a NullPointerException, and the fix.
import java.util.HashMap;
import java.util.Map;
public class WordCount {
public static void main(String[] args) {
Map<String, Integer> counts = new HashMap<>();
for (String word : new String[] {"ant", "bee", "ant"}) {
Integer seen = counts.get(word); // null the first time
counts.put(word, seen == null ? 1 : seen + 1); // int result re-boxed
}
System.out.println("ant=" + counts.get("ant") + " bee=" + counts.get("bee"));
try {
int cats = counts.get("cat") + 1; // hidden intValue() on null
System.out.println(cats);
} catch (NullPointerException e) {
System.out.println("unboxing a missing key threw NullPointerException");
}
int safeCats = counts.getOrDefault("cat", 0) + 1;
System.out.println("safeCats = " + safeCats);
}
}Example explained
Line 1The map is declared with Integer rather than int precisely so a missing key can answer null, which no primitive could express.
Line 2seen == null is a genuine reference test and does not unbox, which is why it is safe to run before any arithmetic.
Line 3seen + 1 unboxes, adds as int, and then put re-boxes the result through Integer.valueOf, so one line contains both conversions.
Line 4counts.get for a missing key returns null and the + operator compiles to intValue() on it; getOrDefault removes the null before it ever reaches that call, which is the fix rather than catching the exception.
Important notes
The shared-instance rule is guaranteed only for boolean, char and byte up to 127, and short and int from -128 to 127; above that range whether == is true is up to the JVM, and HotSpot's -XX:AutoBoxCacheMax can raise the limit, so never let program logic depend on the answer.
Boxing does not chain with widening: a method taking Long cannot be called with the literal 3, because int to Long would need two conversions in a row. Pass 3L instead.
Common mistakes
Comparing two Integer ids or amounts with == and testing only with small values: 12 == 12 passes because both come from the cache, 1200 == 1200 fails in production because two objects were allocated.
Writing map.get(key) + 1 or list.get(i) > 0 with no null check: the NullPointerException is reported on an arithmetic line and beginners waste time looking for a method call that is not written down.
Assuming Integer.valueOf(5).equals(5L) is true: equals compares the runtime class as well as the value, so it returns false and the lookup or contains check silently misses.
Try it yourself
Change, predict, then run
In a browser editor, count the words red, blue, red into a HashMap<String, Integer> using get and put, then print map.get("green") + 1 to watch it throw and rewrite that line with getOrDefault so it prints 1. Then declare two Integer variables holding 127 and two holding 128, print == and equals for each pair, and write a comment saying which result changed and why.
Open the Java workspaceCheck your understanding
A method with return type Integer is assigned into an int variable: int count = findCount(); The line throws NullPointerException even though it contains no visible method call. What is actually happening?
- The compiler turned the assignment into findCount().intValue(), and calling intValue() on a null reference throws.
- The JVM cannot store null in an int slot, so it raises NullPointerException while copying the value.
- int variables hold null until they are assigned, and reading the variable in that same statement throws.
- javac adds a null check in front of every unboxing assignment, and that generated check throws the exception.
Show answer
Unboxing is defined as an invocation of the wrapper's value method, so the exception comes from calling intValue() on null, and the stack trace points at that call. The tempting second option is wrong because the JVM never sees an attempt to put null into an int: the conversion exists only in the code javac generated, so there is no null-to-int store in the bytecode at all.