Wrapper Classes, Autoboxing and Unboxing in Java

Wrapper classes turn primitives into objects so they can be used with generics and collections, and the conversion is automatic but not free.

Why wrappers exist

Generics and collections work only with reference types, so List<int> is impossible. Each primitive therefore has a matching class that holds one value.

PrimitiveWrapper
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

Every wrapper is immutable and final.

Autoboxing and unboxing

Integer boxed = 42;        // autoboxing: Integer.valueOf(42)
int plain = boxed;         // unboxing:   boxed.intValue()

List<Integer> numbers = new ArrayList<>();
numbers.add(7);            // autoboxed
int first = numbers.get(0);  // unboxed

Since Java 5 the compiler inserts these conversions. They are convenient, and they hide three real costs: allocation, a possible NullPointerException, and surprising equality behaviour.

The equality trap

Integer a = 100, b = 100;
System.out.println(a == b);        // true

Integer c = 1000, d = 1000;
System.out.println(c == d);        // false

System.out.println(c.equals(d));   // true
Integer.valueOf caches values from -128 to 127, so small numbers return the same cached object and == appears to work. Above that range each call allocates. This is why == on wrappers is a latent bug rather than an outright error, and why equals is the only correct comparison.

The cache exists for Boolean, Byte, Character up to 127, Short, Integer and Long in the same small range. Float and Double are never cached.

The null trap

Map<String, Integer> counts = new HashMap<>();

int value = counts.get("missing");     // NullPointerException on unboxing

int safe = counts.getOrDefault("missing", 0);   // correct

A wrapper can be null; a primitive cannot. Unboxing a null reference throws, and the stack trace points at an assignment that looks harmless.

Performance

// Boxes and unboxes on every iteration
Long total = 0L;
for (long i = 0; i < 1_000_000; i++) {
    total += i;         // total = Long.valueOf(total.longValue() + i)
}

// One primitive accumulator, no allocation
long total = 0;
for (long i = 0; i < 1_000_000; i++) {
    total += i;
}

The first loop allocates a million objects. Declaring the accumulator as a primitive is the entire fix, and it is a real difference in hot code.

Useful wrapper methods

int parsed = Integer.parseInt("250");          // returns int
Integer boxed = Integer.valueOf("250");        // returns Integer, prefer for caching

System.out.println(Integer.MAX_VALUE);
System.out.println(Integer.toBinaryString(10));   // 1010
System.out.println(Integer.compare(3, 9));        // negative
System.out.println(Integer.parseInt("ff", 16));   // 255

System.out.println(Double.isNaN(0.0 / 0.0));      // true
System.out.println(Character.isDigit('7'));
System.out.println(Boolean.parseBoolean("TRUE")); // true

Prefer parseInt when a primitive is wanted and valueOf when an object is, so no needless boxing occurs either way.

Avoiding boxing in streams

// Boxes every element
int total = numbers.stream().mapToInt(Integer::intValue).sum();

// Primitive stream throughout, no boxing
int sum = IntStream.rangeClosed(1, 100).sum();
OptionalDouble average = IntStream.of(4, 8, 15).average();

IntStream, LongStream and DoubleStream exist precisely to avoid this cost, along with OptionalInt and the primitive functional interfaces such as IntPredicate.

Common mistakes

  • Comparing wrappers with == and being misled by the small value cache.
  • Unboxing a null returned by Map.get.
  • Declaring a loop accumulator as a wrapper type.
  • Using Integer everywhere out of habit when a primitive would do.
  • Calling new Integer(5), which is deprecated for removal; use Integer.valueOf.

Best practices

  • Use primitives unless a reference type is genuinely required.
  • Always compare wrappers with equals, or unbox both sides deliberately.
  • Use getOrDefault or an explicit null check before unboxing.
  • Use primitive streams and primitive functional interfaces in hot code.
  • Reserve wrapper types for cases where null carries real meaning, such as "not supplied".

Practice

  1. Explain why Integer a = 127, b = 127; a == b is true but the same with 128 is false.
  2. Fix int total = map.get(key); so it cannot throw.
  3. Rewrite a boxed accumulation loop with a primitive and describe the difference in allocation.
  4. When is Integer a better field type than int?
  5. What is printed by System.out.println(Double.valueOf(0.0) == Double.valueOf(0.0)); and why?

Conclusion

Wrappers let primitives live in the object world, and autoboxing hides the conversion. Compare with equals, guard against null, and keep primitives in loops and hot paths where the allocation matters.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.