A wrapper class in Java is a class from the java.lang package that holds a single primitive value as an object, such as Integer for int or Boolean for boolean. Autoboxing is the conversion from a primitive to its wrapper that the compiler adds for us, and unboxing is the conversion back.
We use Java wrapper classes wherever the language needs an object, mainly in collections and generic types such as List<Integer>, and wherever a value may be missing and must be null. They also hold the parsing methods and constants for each type, such as Integer.parseInt() and Integer.MAX_VALUE.
The following example boxes and unboxes an int, once by hand and once through a List.
Integer boxed = Integer.valueOf(42); // 42 (int to Integer)
int plain = boxed.intValue(); // 42 (Integer to int)
List<Integer> scores = new ArrayList<>();
boolean added = scores.add(7); // true, autoboxing of 7
int first = scores.get(0); // 7, unboxing
Notice that the last two lines contain no conversion code at all, because the compiler inserts the valueOf() and intValue() calls. We map each primitive to its wrapper, look at why wrappers exist, check where autoboxing and unboxing happen, and finish with the pitfalls that cause real bugs, such as == on Integer values and unboxing null.
1. The Eight Java Wrapper Classes
Java has eight primitive types, and each one has a wrapper class with a matching name. Six of them extend the abstract class Number, which gives them intValue(), longValue(), doubleValue() and the other conversion methods. Character and Boolean have Object as their direct superclass.
| Primitive | Wrapper class | Extends Number | Values cached by valueOf() |
|---|---|---|---|
| byte | Byte | Yes | All 256 values |
| short | Short | Yes | -128 to 127 |
| int | Integer | Yes | -128 to 127 (upper bound configurable) |
| long | Long | Yes | -128 to 127 |
| float | Float | Yes | None |
| double | Double | Yes | None |
| char | Character | No | 0 to 127 |
| boolean | Boolean | No | TRUE and FALSE |
All wrapper objects are immutable, so a method that gets an Integer cannot change the caller’s value. Since Java 16, wrapper classes are value-based classes, which means we compare them with equals() and never use them as locks; javac 25 warns about synchronized on an Integer. The cache column matters for the == pitfall in section 5, and the wrapper class cache article covers it in depth.
2. Why Java Needs Wrapper Classes
Primitives are fast and small, but they are not objects. Generic types accept only reference types, so List<int> does not compile, and a primitive always has a value, so an int field cannot say “unknown”. Wrapper classes fill both gaps.
- Collections and generics store wrappers, as in Map<String, Integer> or Optional<Long>.
- A wrapper can be null, which stands for a missing value in a database column, a JSON field or a form input.
- Each wrapper class holds static helpers and constants for its type, such as Integer.parseInt(), Double.compare(), Character.isDigit() and Long.MAX_VALUE.
- Reflection, serialization libraries and APIs that take Object work with wrappers, because they cannot hold a primitive.
Say an online shop stores a discountPercent column that is empty for most products. With an int field, an empty column turns into 0, and the app can no longer tell “no discount set” from “0% discount”. An Integer field keeps the column empty as null, which is why JPA entities and request DTOs use wrapper types for optional numbers.
3. Converting Between Primitives and Wrappers
Each wrapper offers static factory methods to create an object and instance methods to read the primitive back. The valueOf() methods reuse cached objects, which is why the constructors such as new Integer(10) are deprecated since Java 9 and produce a compiler warning.
Integer count = Integer.valueOf(10); // 10, primitive to wrapper
Integer parsed = Integer.valueOf("25"); // 25, String to wrapper
int raw = Integer.parseInt("25"); // 25, String to primitive
double price = Double.valueOf(9.5).doubleValue(); // 9.5, wrapper to primitive
long asLong = count.longValue(); // 10, widening through Number
String text = Integer.toString(255, 16); // "ff"
boolean flag = Boolean.parseBoolean("TRUE"); // true, case-insensitive
Integer bad = Integer.valueOf("ten"); // NumberFormatException: For input string: "ten"
We pick parseInt() when we need an int and valueOf() when we need an Integer. Both throw NumberFormatException on invalid text, so code that reads user input checks the text first or catches the exception, as the String to int guide shows.
4. Autoboxing and Unboxing
Autoboxing and unboxing were added in Java 5. Wherever the code has a primitive and the context expects its wrapper, the compiler inserts a call to valueOf(). Wherever it has a wrapper and the context expects a primitive, the compiler inserts a call such as intValue(). The rules are boxing conversion and unboxing conversion in the language specification.

4.1. Where Autoboxing Happens
Autoboxing happens on assignment, when we pass an argument, when we return a value and when we add to a collection. Each line in the following snippet boxes an int.
Integer level = 3; // 3, assignment
List<Integer> stock = new ArrayList<>();
boolean ok = stock.add(12); // true, argument
Map<String, Integer> prices = new HashMap<>();
Integer old = prices.put("apple", 5); // null, no previous value
Object any = 99; // 99, boxed to Integer first
String type = any.getClass().getSimpleName(); // "Integer"
4.2. Where Unboxing Happens
Unboxing happens when a wrapper is used in arithmetic, in a comparison such as <, in a condition, in a switch, or when it is assigned to a primitive variable.
Integer a = 20;
Integer b = 22;
int sum = a + b; // 42
boolean bigger = a < b; // true
Boolean enabled = Boolean.TRUE;
String mode = enabled ? "on" : "off"; // "on"
Map<String, Integer> stock = Map.of("apple", 5);
int apples = stock.get("apple"); // 5
4.3. What the Compiler Generates
Autoboxing is a compile-time feature, so the class file contains plain method calls. The following method adds an int to a list and reads it back.
static int firstScore(List<Integer> scores) {
scores.add(10);
int first = scores.get(0);
return first;
}
Running javap -c on the compiled class shows the calls that the compiler inserted, namely Integer.valueOf before add() and Integer.intValue after get().
$ javap -c Boxing
1: bipush 10
3: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
6: invokeinterface #13, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
14: invokeinterface #19, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
19: checkcast #8 // class java/lang/Integer
22: invokevirtual #23 // Method java/lang/Integer.intValue:()I
Because these are ordinary calls, everything that applies to them applies to autoboxing too. A valueOf() call may create an object, and an intValue() call on null throws a NullPointerException.
5. Common Wrapper Class Pitfalls
Autoboxing hides the conversion calls, so the bugs it causes are hard to spot in code review. Each of the following cases compiles without an error.
5.1. Comparing Wrappers With ==
The == operator compares references when both sides are wrapper objects. Values from -128 to 127 come from the cache, so two boxed 127 values are the same object, whereas two boxed 128 values are two different objects.
Integer small1 = 127;
Integer small2 = 127;
boolean sameSmall = small1 == small2; // true, cached object
Integer big1 = 128;
Integer big2 = 128;
boolean sameBig = big1 == big2; // false, two objects
boolean equalBig = big1.equals(big2); // true
boolean mixed = big1 == 128; // true, big1 is unboxed
Compare wrapper values with equals() or Objects.equals(), never with ==. Tests with small numbers pass with ==, and the bug shows up only in production, when an order id or a score passes 127.
5.2. Unboxing a null Value
Unboxing calls a method on the wrapper, so a null wrapper throws a NullPointerException. The most common source is Map.get() for a missing key, and a ternary expression that mixes int and Integer also unboxes.
Map<String, Integer> stock = new HashMap<>();
int pears = stock.get("pear"); // NullPointerException
int safe = stock.getOrDefault("pear", 0); // 0
Integer discount = null;
boolean member = false;
int percent = member ? 10 : discount; // NullPointerException
The helpful NullPointerException message names the call, such as Cannot invoke “java.lang.Integer.intValue()” because the return value of “java.util.Map.get(Object)” is null, which points straight at the unboxing.
5.3. equals() Across Wrapper Types
The equals() method of a wrapper returns true only for an object of the same wrapper class. A Long never equals an Integer, even with the same number, which breaks lookups in a Map<Long, …> with an int key.
Long id = 10L;
boolean sameType = id.equals(10L); // true
boolean intArg = id.equals(10); // false, 10 is boxed to Integer
Map<Long, String> users = Map.of(10L, "Anna");
String found = users.get(10); // null, key is an Integer
String fixed = users.get(10L); // "Anna"
5.4. List.remove(int) and remove(Object)
The List interface has two remove() methods. With an int argument, the compiler picks remove(int index) and does not box the value, so we wrap the value ourselves when we mean the element.
List<Integer> codes = new ArrayList<>(List.of(10, 20, 30));
Integer removed = codes.remove(1); // 20, removed by index
boolean found = codes.remove(Integer.valueOf(10)); // true, removed by value
String left = codes.toString(); // "[30]"
5.5. Boxing in Loops
A loop that adds to a Long variable unboxes and boxes on every step, and values above 127 create a new object each time. For counters and sums in hot code, we use the primitive type and box once at the end, or use primitive streams such as IntStream that avoid boxing.
List<Integer> prices = List.of(120, 340, 99);
long total = 0;
for (int p : prices) {
total += p;
}
long result = total; // 559
int streamSum = prices.stream().mapToInt(Integer::intValue).sum(); // 559
6. Useful Wrapper Class Methods
Wrapper classes are also the home of most number and character utilities. The static methods take primitives, so they do not create objects.
| Method or constant | Example | Result |
|---|---|---|
| Integer.parseInt(s, radix) | Integer.parseInt(“ff”, 16) | 255 |
| Integer.compare(a, b) | Integer.compare(3, 7) | -1 |
| Integer.sum(a, b) | Integer.sum(2, 3) | 5 |
| Integer.toBinaryString(i) | Integer.toBinaryString(10) | “1010” |
| Integer.MAX_VALUE | Integer.MAX_VALUE | 2147483647 |
| Character.isDigit(c) | Character.isDigit(‘7’) | true |
| Double.isNaN(d) | Double.isNaN(0.0 / 0.0) | true |
| Boolean.parseBoolean(s) | Boolean.parseBoolean(“yes”) | false |
int hex = Integer.parseInt("ff", 16); // 255
int order = Integer.compare(3, 7); // -1
String bits = Integer.toBinaryString(10); // "1010"
boolean digit = Character.isDigit('7'); // true
boolean nan = Double.isNaN(0.0 / 0.0); // true
boolean yes = Boolean.parseBoolean("yes"); // false
7. Wrapper Class FAQs
Beginners mostly ask how a wrapper differs from its primitive and whether types such as String count as wrappers.
7.1. Is String a Wrapper Class?
No. String is an immutable class for text, but it does not wrap a primitive type, and there is no autoboxing between char[] and String. The eight wrapper classes are the ones in section 1.
7.2. What Is the Difference Between int and Integer?
An int is a primitive that holds the number itself, takes 4 bytes and can never be null. An Integer is an object that holds an int, can be null, works in collections and generics, and is compared with equals(). We use int for calculations and local variables, and Integer where an object or a missing value is needed.
7.3. Why Is new Integer() Deprecated?
The constructor always creates a new object, while Integer.valueOf() can return a cached one, which saves memory for common values. The constructors of all wrapper classes are deprecated since Java 9, and javac prints a deprecation warning for each use. The draft feature JEP 401, which is not part of Java 25, proposes value objects, where identity-based code such as new Integer(5) == new Integer(5) makes no sense.
7.4. Are Wrapper Objects Immutable?
Yes. Every wrapper class stores its value in a final field and has no setter. An expression such as count++ on an Integer unboxes the value, adds one and boxes a new object, so other references to the old object still see the old value.
7.5. Can We Use Primitives in Generics?
No, not in Java 25. Type arguments must be reference types, so we write List<Integer> and let autoboxing convert each value. For large numeric data, primitive arrays and primitive streams such as IntStream avoid the boxing cost.
8. Conclusion
Java wrapper classes turn the eight primitive types into objects, so we can store numbers in collections, use them as type arguments and represent a missing value with null. We create them with valueOf(), read them with methods such as intValue(), and leave the deprecated constructors alone.
Autoboxing and unboxing are compiler features that insert those same calls for us. That convenience comes with four traps, namely == comparing references, null values throwing NullPointerException, equals() failing across wrapper types and remove(int) picking the index. For the rest of the language basics, see the Java tutorial.
9. References
- Integer Javadoc (Java 25)
- Number Javadoc
- Value-based classes
- JLS 5.1.7 Boxing Conversion
- Oracle tutorial on autoboxing and unboxing
Happy Learning !!