Wrapper Classes & Autoboxing
Reviewed & published by Brayan K
Java's eight primitives are fast but they aren't objects — so they can't go in a List or be null. Wrapper classes like Integer wrap a primitive in an object, and autoboxing converts between the two automatically. Convenient — and full of subtle traps.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You'll need the primitive types (int, double, boolean, char...) and a first look at collections like ArrayList, which only hold objects — the main reason wrappers exist.
What You'll Learn
- The wrapper class for each of the 8 primitives
- Autoboxing and auto-unboxing — what the compiler does for you
- Why collections force you to use wrappers
- The == cache trap and why you must use .equals()
- The NullPointerException hiding inside null unboxing
- Useful static helpers: parseInt, MAX_VALUE, toBinaryString
The Big Idea — A Gift Box for a Number
💡 Analogy: A primitive int is a bare coin sitting on the table — quick to grab, but it can't be labelled or shipped. An Integer is that same coin sealed in a small gift box: now it has a label, you can put it on a shelf alongside other boxes (a collection), and the box can be empty (null). Autoboxing is the machine that drops the coin in the box; unboxing takes it back out.
Every primitive has a matching wrapper: int→Integer, double→Double, boolean→Boolean, char→Character, long→Long, short→Short, byte→Byte, float→Float. They live in java.lang, so no import is needed.
1️⃣ The Eight Wrapper Classes
Each primitive maps to exactly one wrapper class. Most names just capitalise the primitive; the two exceptions are int→Integer and char→Character.
| Primitive | Wrapper | Note |
|---|---|---|
| int | Integer | Name differs from the primitive |
| char | Character | Name differs from the primitive |
| double | Double | Just capitalised |
| boolean | Boolean | Just capitalised |
| long | Long | Just capitalised |
| short | Short | Just capitalised |
| byte | Byte | Just capitalised |
| float | Float | Just capitalised |
public class Main {
public static void main(String[] args) {
// A primitive and its wrapper object
int primitive = 42;
Integer wrapped = Integer.valueOf(42); // explicit boxing
System.out.println("primitive int : " + primitive);
System.out.println("Integer object: " + wrapped);
// Each primitive has a matching wrapper class:
Integer i = 100;
Double d = 3.14;
Boolean b = true;
Character c = 'A';
Long l = 9000000000L;
System.out.println("Integer : " + i);
System.out.println("Double : " + d);
System.out.println("Boolean : " + b);
System.out.println("Character: " + c);
System.out.println("Long : " + l);
}
}2️⃣ Autoboxing & Auto-Unboxing
Since Java 5, the compiler converts between a primitive and its wrapper automatically wherever the context demands it. Autoboxing wraps a primitive into an object; auto-unboxing pulls the primitive back out.
Integer boxed = 5; // autoboxing: Integer.valueOf(5)
int back = boxed; // auto-unboxing: boxed.intValue()This is why you can write list.add(90) even though a List<Integer> only stores objects — the 90 is boxed for you. The convenience is real, but remember it's real method calls under the hood.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
// AUTOBOXING: int -> Integer happens automatically
Integer boxed = 5; // really Integer.valueOf(5)
// AUTO-UNBOXING: Integer -> int happens automatically
int unboxed = boxed; // really boxed.intValue()
System.out.println("boxed=" + boxed + ", unboxed=" + unboxed);
// Collections can only hold OBJECTS, so primitives are auto-boxed:
List<Integer> scores = new ArrayList<>();
scores.add(90); // int 90 -> Integer.valueOf(90)
scores.add(75);
scores.add(88);
int total = 0;
for (int s : scores) { // each Integer auto-unboxes to int
total += s;
}
double average = (double) total / scores.size();
System.out.println("Total: " + total);
System.out.printf("Average: %.1f%n", average);
}
}3️⃣ The == Trap (Use .equals!)
This is the most infamous wrapper gotcha. For memory efficiency, Java caches Integer objects for the range -128 to 127. Two boxed values in that range share one cached object, so == (which compares references) returns true. Outside the range, each box is a new object and == returns false — even when the numbers are equal.
public class Main {
public static void main(String[] args) {
// The == trap with wrapper objects.
// Java caches Integer values from -128 to 127, so small equal
// values share one cached object — but larger ones do not.
Integer a = 100;
Integer b = 100;
System.out.println("100 == 100 : " + (a == b)); // cached -> true
Integer c = 200;
Integer d = 200;
System.out.println("200 == 200 : " + (c == d)); // not cached -> false
// .equals() compares the VALUE, so it is always correct:
System.out.println("200.equals : " + c.equals(d)); // true
// Comparing a wrapper to a primitive auto-unboxes, so == is fine there:
int primitive = 200;
System.out.println("c == 200 : " + (c == primitive)); // unboxes -> true
}
}4️⃣ The null Danger
A wrapper can hold null — useful for "no value yet". But the moment Java tries to auto-unbox a null wrapper into a primitive, it calls a method on null and throws a NullPointerException. This is one of the sneakiest NPEs because the unboxing is invisible in the source.
public class Main {
public static void main(String[] args) {
// A wrapper can be null — a primitive cannot. That extra "null" is
// useful for "no value yet", but unboxing a null EXPLODES.
Integer maybe = null;
System.out.println("maybe is null? " + (maybe == null));
try {
int x = maybe; // auto-unboxing null -> NullPointerException
System.out.println(x);
} catch (NullPointerException e) {
System.out.println("Boom! Unboxing null threw a NullPointerException");
}
}
}5️⃣ Free Utilities on Wrapper Classes
Wrapper classes aren't just boxes — they're toolboxes of static helpers you'll use constantly: parse text into numbers, read type limits, and convert between number bases.
- Integer.parseInt("256") — text → int (throws if not a number)
- Integer.MAX_VALUE / MIN_VALUE — the type's bounds
- Integer.toBinaryString(255), toHexString(255) — base conversions
- Integer.compare(a, b) — returns -1, 0, or 1 for sorting
public class Main {
public static void main(String[] args) {
// Wrapper classes carry handy STATIC utilities.
// Parsing text into numbers:
int n = Integer.parseInt("256");
double price = Double.parseDouble("19.99");
System.out.println("parsed int : " + n);
System.out.println("parsed double: " + price);
// Min / max constants:
System.out.println("Integer.MAX_VALUE: " + Integer.MAX_VALUE);
System.out.println("Integer.MIN_VALUE: " + Integer.MIN_VALUE);
// Base conversions:
System.out.println("255 in binary: " + Integer.toBinaryString(255));
System.out.println("255 in hex : " + Integer.toHexString(255));
// Comparing and bounds:
System.out.println("max(7, 3) : " + Integer.max(7, 3));
System.out.println("compare(5,9) : " + Integer.compare(5, 9));
}
}🧩 Reorder Challenge
These lines read three String prices, parse them, and print the total. They've been shuffled — find the right order.
A) total += Double.parseDouble(p);
B) double total = 0;
C) String[] prices = {"4.50", "3.25", "2.00"};
D) System.out.printf("Total: %.2f%n", total);
E) for (String p : prices) {
F) }Correct order: C, B, E, A, F, D
String[] prices = {"4.50", "3.25", "2.00"};
double total = 0;
for (String p : prices) {
total += Double.parseDouble(p);
}
System.out.printf("Total: %.2f%n", total);
// Output: Total: 9.75Declare the data and accumulator, loop and parse each string with the wrapper's static method, close the loop, then print.
🧠 Quick Recall
Predict the output before opening each answer.
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
System.out.println((a == b) + " " + (c == d));Answer: true false. 127 is inside the cache (-128..127) so a and b share one object. 128 is outside it, so c and d are separate objects and == compares references.
Integer count = null;
int n = count + 1;
System.out.println(n);Answer: It throws a NullPointerException. count + 1 needs to auto-unbox count to an int, but it's null — so the invisible count.intValue() call blows up before anything is printed.
String s = "42";
int doubled = Integer.parseInt(s) * 2;
System.out.println(doubled);Answer: 84. parseInt turns the text "42" into the int 42, which is then doubled. (If s were "4two" it would throw a NumberFormatException instead.)
🎯 Your Turn #1 — Parse and sum
Three numbers arrive as text. Parse each with a wrapper method and print the total.
public class Main {
public static void main(String[] args) {
// YOUR TURN: these three lines come as String input (e.g. from a form).
// Parse each one to a number, add them, and print the total.
// Expected output: Total: 60
String aText = "10";
String bText = "20";
String cText = "30";
int total = 0; // parse and sum here
System.out.println("Total: " + total);
}
}🎯 Your Turn #2 — Compare correctly
The values are 500 — outside the cache — so == gives the wrong answer. Fix the comparison to compare values.
public class Main {
public static void main(String[] args) {
// YOUR TURN: x and y are Integer objects holding the value 500.
// 500 is OUTSIDE the cache range, so == will be false.
// Print "equal" if their VALUES match, otherwise "different".
// Expected output: equal
Integer x = 500;
Integer y = 500;
// replace this line with a correct value comparison:
String result = (x == y) ? "equal" : "different";
System.out.println(result);
}
}🧩 Mini-Challenge — Nullable max
Return type Integer lets you return null for an empty list — something an int can't do. Build it from the outline.
import java.util.List;
public class Main {
// GOAL: write findMax that returns the largest value, or null for an
// empty list. Returning Integer (not int) lets you return null.
//
// Expected output:
// Max of [3, 9, 1, 7]: 9
// Max of empty list : null
static Integer findMax(List<Integer> numbers) {
// 1. if numbers.isEmpty() return null
// 2. otherwise loop and track the largest (auto-unboxing as you compare)
// 3. return the result
return null; // your code here
}
public static void main(String[] args) {
System.out.println("Max of [3, 9, 1, 7]: " + findMax(List.of(3, 9, 1, 7)));
System.out.println("Max of empty list : " + findMax(List.of()));
}
}Common Errors
- ❌ Comparing wrappers with ==. Works by luck for small values, fails for large ones. Fix: always use .equals() for wrapper values.
- ❌ NullPointerException from unboxing. A null Integer used in arithmetic or assigned to an int throws. Fix: null-check before unboxing, or keep the variable as a primitive.
- ❌ List<int> won't compile. Generics need object types. Fix: use List<Integer> — the elements autobox automatically.
- ❌ NumberFormatException from parseInt. Integer.parseInt("12a") throws at runtime. Fix: validate input, or wrap the call in a try-catch.
- ❌ Heavy boxing in tight loops. Integer sum = 0; inside a million-iteration loop allocates a million objects. Fix: accumulate in a primitive int and box once at the end if needed.
- ❌ Using new Integer(5). Deprecated since Java 9 and always creates a new object (defeating the cache). Fix: use Integer.valueOf(5) or just 5.
Pro Tips
- 💡 Default to primitives for fields and local variables; reach for wrappers only when an API or nullability demands it.
- 💡 An Optional<Integer> often communicates "value may be absent" more clearly than a nullable Integer.
- 💡 For high-performance numeric work, use primitive streams (IntStream, DoubleStream) to avoid boxing entirely.
- 💡 The cache range is configurable upward via -XX:AutoBoxCacheMax, but never rely on cache behaviour for correctness — always use .equals().
📋 Quick Reference
| Task | Syntax | Notes |
|---|---|---|
| Box a primitive | Integer i = 5; | Autoboxing — Integer.valueOf(5) |
| Unbox to primitive | int n = i; | Auto-unboxing — i.intValue() |
| Compare values | a.equals(b) | Never == for wrappers! |
| Parse text | Integer.parseInt("12") | NumberFormatException if bad |
| Type bounds | Integer.MAX_VALUE | 2147483647 |
| Collection type | List<Integer> | Wrappers only, never primitives |
🎉 Lesson Complete!
You now know every primitive's wrapper class, how autoboxing and auto-unboxing work, why collections force wrappers on you, and the two big traps — the == cache surprise and the null-unboxing NPE. You can also reach for the handy static helpers like parseInt and MAX_VALUE.
Practice quiz
What does this print? Integer a = 100; Integer b = 100; System.out.println(a == b);
- false
- It throws an exception
- true
- 100
Answer: true. Java caches Integer values from -128 to 127, so a and b share one cached object and == returns true. This is the cache trap.
What does this print? Integer c = 200; Integer d = 200; System.out.println(c == d);
- false
- true
- 200
- It throws an exception
Answer: false. 200 is OUTSIDE the cache range (-128..127), so c and d are separate objects and == compares references, giving false. Use .equals() instead.
What is autoboxing?
- Converting a wrapper to a primitive
- Comparing two wrappers
- Parsing a String into a number
- The compiler automatically converting a primitive to its wrapper, e.g. int 5 to Integer.valueOf(5)
Answer: The compiler automatically converting a primitive to its wrapper, e.g. int 5 to Integer.valueOf(5). Autoboxing wraps a primitive into its wrapper object where an object is expected; auto-unboxing is the reverse (Integer to int via intValue()).
What happens here? Integer count = null; int n = count + 1;
- n becomes 1
- It throws a NullPointerException when auto-unboxing null
- n becomes null
- It prints 0
Answer: It throws a NullPointerException when auto-unboxing null. count + 1 must auto-unbox count to an int, calling the invisible count.intValue() on null, which throws a NullPointerException.
Why does == sometimes work and sometimes fail with Integer?
- Values -128..127 are cached and share an object, but outside that range each box is new
- It depends on the day
- == never works with Integer
- It depends on the variable names
Answer: Values -128..127 are cached and share an object, but outside that range each box is new. Cached small values share one object so == is true; outside the cache each autoboxed value is a new object so == compares references. Always use .equals().
What does Integer.toBinaryString(255) return?
- "FF"
- "255"
- "11111111"
- "0xFF"
Answer: "11111111". 255 in binary is eight 1 bits: "11111111". (Integer.toHexString(255) would give "ff".)
Why does List<Integer> work but List<int> does not?
- int is deprecated
- Generics require object types, so primitives must use their wrapper classes
- Integer is faster
- List cannot hold numbers
Answer: Generics require object types, so primitives must use their wrapper classes. Generic collections can only hold objects, not primitives, so you use List<Integer>; the int elements autobox automatically on add.
What does Integer.parseInt("256") return, and what type is it?
- "256" as a String
- 256 as an Integer object
- It throws an exception
- 256 as an int
Answer: 256 as an int. parseInt turns the text "256" into the primitive int 256. (parseInt("12a") would throw NumberFormatException.)
Why is new Integer(5) discouraged in modern Java?
- It is too slow to type
- It is deprecated since Java 9 and always creates a new object, defeating the cache
- It returns null
- It throws an exception
Answer: It is deprecated since Java 9 and always creates a new object, defeating the cache. new Integer(5) is deprecated and always allocates a fresh object. Use Integer.valueOf(5) or just 5, which can reuse the cache.
When should you prefer primitives over wrappers?
- Never - always use wrappers
- Only inside collections
- By default in loops, counters, and local variables, to avoid allocation and null/equality traps
- Only when a value may be null
Answer: By default in loops, counters, and local variables, to avoid allocation and null/equality traps. Prefer primitives by default; reach for wrappers only when an API forces it (generics/collections) or when a value genuinely needs to be nullable.
Continue this course
- Previous: Switch Statement & Expressions
- Next: Methods — Write reusable methods, pass parameters, and return values
- Quick reference: Java cheat sheet
Frequently asked questions
What is the difference between int and Integer?
int is a primitive — it stores a raw 32-bit value directly and is fast and memory-light. Integer is an object that wraps an int; it lives on the heap, can be null, and carries useful methods and constants. Use int for plain arithmetic and local variables; use Integer when you need an object, such as inside generic collections (List<Integer>) or when a value might be absent (null).
What is autoboxing and auto-unboxing?
Autoboxing is the compiler automatically converting a primitive to its wrapper (int 5 becomes Integer.valueOf(5)) where an object is expected. Auto-unboxing is the reverse — a wrapper is converted back to a primitive (Integer to int via intValue()) where a primitive is expected. It lets you write list.add(5) instead of list.add(Integer.valueOf(5)), but it hides real method calls, so it has a small cost and can throw a NullPointerException when unboxing null.
Why does == sometimes work and sometimes fail with Integer?
Java caches Integer objects for values from -128 to 127. Two Integers with the same small value share one cached object, so == returns true. Outside that range each autoboxed value is a new object, so == compares references and returns false even when the values match. The rule: never use == to compare wrapper values — always use .equals() (or unbox to primitives first).
Can a wrapper be null and why does that matter?
Yes. Unlike a primitive, an Integer can be null, which is handy for representing 'no value yet'. The danger is auto-unboxing: if you assign a null Integer to an int, or do arithmetic on it, Java tries to call intValue() on null and throws a NullPointerException. Always null-check a wrapper before unboxing it.
When should I prefer primitives over wrappers?
Prefer primitives by default — in loops, counters, arithmetic, and local variables — because they avoid object allocation and the null/equality traps. Reach for wrappers only when the API forces it: generics and collections (which can't hold primitives), or when a value genuinely needs to be nullable. Avoid declaring fields as wrappers unless null is a meaningful state.