Wrapper Classes ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-15

Autoboxing and Unboxing in Java

For Integer a = 1000, b = 1000; the expressions a <= b and a >= b both return true, yet a == b returns false. It is the same number, but in the first two comparisons the compiler unboxed both wrappers to int, while in the third it compared references to two different objects. That is autoboxing working quietly behind your back.

Autoboxing in Java is the automatic conversion of a primitive value into an object of the corresponding wrapper class: int → Integer, double → Double. Unboxing is the reverse: converting a wrapper object back into a primitive. Java autoboxing and unboxing were introduced in Java 5 and are done by the compiler, which inserts calls to Integer.valueOf() and intValue() into your code.

What Is Autoboxing and Unboxing in Java

Autoboxing and unboxing are implicit conversions between a primitive and its wrapper that the Java compiler adds on its own: wherever an Integer is expected but you pass an int, the value is boxed; wherever an int is needed but you have an Integer, it is unboxed. Here is a minimal autoboxing and unboxing in Java example:

public class AutoBoxExample1 {
    public static void main(String[] args) {
        Integer iOb = 100; // autoboxing: int -> Integer
        int i = iOb;       // unboxing: Integer -> int
        System.out.println(i + " " + iOb);
    }
}

Output:

100 100

What the Compiler Turns Autoboxing Into

Boxing and unboxing are performed by the javac compiler (part of the JDK), not by the JVM at run time. To box a value it calls the static valueOf() method; to unbox it calls intValue(), doubleValue(), booleanValue() or another xxxValue() method:

// What you write
Integer iOb = 100;
int i = iOb;
iOb++;

// What the compiler actually generates
Integer iOb = Integer.valueOf(100);
int i = iOb.intValue();
iOb = Integer.valueOf(iOb.intValue() + 1);

You can see this for yourself by disassembling the class: the bytecode contains ordinary calls to java/lang/Integer.valueOf and java/lang/Integer.intValue.

javac AutoBoxExample1.java
javap -c AutoBoxExample1

Where Autoboxing and Unboxing Happen

The conversion kicks in anywhere the type of an expression differs from the expected type but the two form a primitive–wrapper pair:

Context Example What the compiler does
Assignment to a wrapper Integer x = 5; Integer.valueOf(5)
Assignment to a primitive int n = x; x.intValue()
Method argument process(5) for process(Integer v) boxes the argument
Return value return value; (method returns int, value is an Integer) unboxes the returned value
Arithmetic and <, >, <=, >= x + 1, x < y unboxes the operands, boxes the result if needed
if, while, for conditions, ternary operator if (flag) with Boolean flag flag.booleanValue()
== between a wrapper and a primitive x == 5 unboxes x and compares values
Collections and generics list.add(5), int n = list.get(0) boxes on add, unboxes on read (see Wrappers in Collections and Generics)

Both literals and primitive variables can be autoboxed:

Integer iOb1 = 100; // literal
int i = 200;
Integer iOb2 = i;   // variable

Boxing a variable requires an exact type match: a byte boxes only into Byte, an int only into Integer (the full list of pairs is in the primitive-to-wrapper table). Trying to box a byte variable into a Short without an explicit cast fails with incompatible types: byte cannot be converted to Short:

byte b = 4;
// Short s1 = b;      // compilation error
Short s2 = (short) b; // cast byte -> short first, then box

Easy to miss

Short s = 4; and Character c = 65; compile: for constants that fit the target type, the specification (JLS 5.2) allows narrowing followed by boxing into Byte, Short or Character. But Long l = 4; and Double d = 1; fail with incompatible types: int cannot be converted to Long: widening int → long and then boxing is not allowed. Write Long l = 4L; and Double d = 1.0; instead.

The same rule — you may box and then widen to a supertype such as Object, but you may not widen a primitive and then box — also affects which overloaded method the compiler picks when a call needs widening, boxing or varargs.

Autoboxing in Methods and Expressions: Examples

When you call a method, the argument is boxed, and the return value is unboxed or boxed depending on the types involved:

public class AutoBoxAndMethods {
    static int someMethod(Integer value) {
        return value;                 // unboxing Integer -> int
    }

    public static void main(String[] args) {
        Integer iOb = someMethod(100); // 100 is boxed for the argument, the int result is boxed again
        System.out.println(iOb);       // 100
    }
}

Inside an expression a wrapper is unboxed automatically, the calculation runs on primitives, and the result is boxed again if needed:

public class AutoBoxAndOperations {
    public static void main(String[] args) {
        Integer iOb1 = 100;

        Integer iOb2 = iOb1 + iOb1 / 3; // 100 + 33, the result is boxed
        System.out.println("iOb2 after the expression: " + iOb2);

        int i = iOb1 + iOb1 / 3;        // the result stays an int
        System.out.println("i after the expression: " + i);
    }
}

Output (integer division, so 100 / 3 = 33):

iOb2 after the expression: 133
i after the expression: 133

A Boolean object can be used as a condition in an if statement, in while, do-while and for loops and in the ternary operator — it is unboxed to boolean. Character works the same way:

public class AutoBoxAndCharacters {
    public static void main(String[] args) {
        Boolean b = true;
        if (b) {                        // b.booleanValue()
            System.out.println("Unboxing works in if, too.");
        }

        Character ch = 'x';             // Character.valueOf('x')
        char ch2 = ch;                  // ch.charValue()
        System.out.println("ch2 = " + ch2);
    }
}

Output:

Unboxing works in if, too.
ch2 = x

Before Java 5 and Today

Before Java 5 there was no autoboxing: to increment a wrapper you had to unbox it by hand, change the primitive and create a new object. Old code did this with the new Integer(...) constructor, which is now obsolete — why and what replaced it is covered in valueOf() instead of constructors.

// How it was written before Java 5 (do not use in new code)
Integer y = new Integer(567);  // deprecated constructor
int x = y.intValue();
x++;
y = new Integer(x);

The same code with autoboxing:

public class AutoBoxExample2 {
    public static void main(String[] args) {
        Integer y = 567;               // Integer.valueOf(567)
        y++;                           // y = Integer.valueOf(y.intValue() + 1)
        System.out.println("y = " + y);
    }
}

Output:

y = 568

The code is shorter, but it does the same thing: it unboxes, increments and boxes the result into a new object. The next section explains why it has to be a new one.

Wrapper Objects Are Immutable: What Happens on i++

Wrapper objects are immutable: the value inside an Integer cannot change after the object is created. So y++ on an Integer variable does not modify the old object — it makes the variable point to another object holding the new value. Any other reference to the old object still sees the old value:

public class AutoBoxImmutability {
    public static void main(String[] args) {
        Integer y = 567;
        Integer x = y;
        System.out.println(y == x);  // true: x and y refer to the same object

        y++;
        System.out.println(x + " " + y);
        System.out.println(y == x);  // false: y now refers to a new object
    }
}

Output:

true
567 568
false

Let's look at the memory. After the line

Integer y = 567;

the variable y points to an Integer object with the value 567:

Integer variable y points to an object with the value 567 in memory

After y++ another object appears in memory, and y now points to it. The old object has not changed: if nothing else refers to it, the garbage collector will eventually remove it.

Integer y = 567;
y++;

After y++ the variable y points to a different Integer object in memory

For small values, the «new» object may turn out to be an existing object from the cache — that is the topic of the next section.

The Integer Cache: Which Values valueOf() Reuses

The valueOf() method that autoboxing calls does not always create a new object: for small values it returns a pre-built object from a cache. For Integer, Short, Byte and Long the cached range is -128 to 127, for Character it is 0 to 127, and Boolean has just two constants. A value outside the range is boxed into a new object.

Class Cached values Guaranteed by
Boolean Boolean.TRUE and Boolean.FALSE language specification
Byte all values, -128 to 127 language specification
Short -128 to 127 language specification
Integer -128 to 127; the upper bound can be raised with the JVM option -XX:AutoBoxCacheMax language specification (for -128..127)
Long -128 to 127 JDK implementation only (the Long.valueOf() javadoc does not promise a cache)
Character \u0000 to \u007f (0 to 127) language specification
Float, Double not cached every boxing creates a new object

The guaranteed ranges are listed in JLS 5.1.7 Boxing Conversion. Here is how the cache affects reference comparison:

public class AutoBoxExampleCaching {
    public static void main(String[] args) {
        Integer i1 = 23;
        Integer i2 = 23;
        System.out.println(i1 == i2);      // the same cached object
        System.out.println(i1.equals(i2));

        Integer i3 = 2300;
        Integer i4 = 2300;
        System.out.println(i3 == i4);      // two different objects
        System.out.println(i3.equals(i4));

        Double d1 = 1.0;
        Double d2 = 1.0;
        System.out.println(d1 == d2);      // Double is not cached
    }
}

Output with default JVM settings:

true
true
false
true
false

The deprecated new Integer(23) constructor bypasses the cache and always creates a new object, so new Integer(23) == new Integer(23) is false even for small numbers. Autoboxing, on the other hand, always goes through valueOf() and the cache.

Tuning the cache

The -XX:AutoBoxCacheMax=10000 option (or the java.lang.Integer.IntegerCache.high system property) raises only the upper bound and only for Integer; the lower bound of -128 and the caches of Short, Long and Character stay the same. This means the result of i3 == i4 for 2300 depends on launch options — code that relies on it may behave differently on different servers.

Comparing Wrappers: == or equals()

The == operator applied to two wrappers compares references, not values, so the result depends on the cache. If one operand is a primitive, the wrapper is unboxed and the values are compared. The equals() method compares both value and type. The safe rule: compare two wrappers with equals() or Objects.equals().

public class WrapperComparison {
    public static void main(String[] args) {
        Integer a = 1000;
        Integer b = 1000;
        System.out.println(a == b);
        System.out.println(a <= b && a >= b);
        System.out.println(a.equals(b));
        System.out.println(a == 1000);
        System.out.println(a.intValue() == b);

        Long l = 1000L;
        System.out.println(l == 1000);
        System.out.println(l.equals(1000));
    }
}

Output:

false
true
true
true
true
true
false
Expression Result Why
a == b false two wrappers: references are compared; 1000 is outside the cache, so these are different objects
a <= b && a >= b true <= and >= are not defined for references, so both wrappers are unboxed
a.equals(b) true values are compared
a == 1000 true one operand is a primitive, so a is unboxed
a.intValue() == b true the left side is an int, so b is unboxed
l == 1000 true l is unboxed to long, 1000 is widened to long
l.equals(1000) false 1000 is boxed into an Integer, and an Integer is never equal to a Long

To order wrappers, use compareTo() or Integer.compare(a, b). If either wrapper may be null, Objects.equals(a, b) is safer than a.equals(b), and comparing with a primitive (a == 1000) throws an exception when a is null — see the next section.

NullPointerException When Unboxing null

Unboxing is a hidden call to intValue() (or another xxxValue() method) on an object. If the wrapper variable is null, there is no object to call the method on, so unboxing throws a NullPointerException even though the line contains no visible method call. A primitive cannot hold null, so the language has no «soft» form of unboxing.

public class UnboxingNull {
    static Integer findDiscount() {
        return null;                 // no discount found
    }

    public static void main(String[] args) {
        int discount = findDiscount(); // NullPointerException
    }
}

Since Java 14 (enabled by default since Java 15), the NPE message tells you exactly what was being unboxed:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "UnboxingNull.findDiscount()" is null

The same error shows up in less obvious places:

  • A Boolean condition. Boolean enabled = null; if (enabled) { ... } — NPE on booleanValue().
  • Arithmetic and comparison with a primitive. Integer count = null; count++; or count > 0 — the wrapper is unboxed before the operation.
  • A ternary operator with mixed types. If one operand is an Integer and the other is an int, the whole expression has type int, and the wrapper operand is unboxed even when the result is assigned to an Integer:
Integer cached = null;
boolean useCache = true;

// Integer value = useCache ? cached : -1;              // NPE: the expression type is int
Integer value = useCache ? cached : Integer.valueOf(-1); // both operands are Integer
System.out.println(value);                               // null

A short version of this rule with a Map example is in the Wrapper Classes in Java lesson.

Unboxing safely

Supply a default value before unboxing: int n = Objects.requireNonNullElse(count, 0); (Java 9+), int age = map.getOrDefault(key, 0); or Optional.ofNullable(count).orElse(0). Better still, don't allow null where a number is always expected: declare the field or variable as int.

Java Autoboxing Performance: Boxing in Loops

Autoboxing is not free: every boxing of a value outside the cache allocates a new object on the heap. On 64-bit HotSpot an Integer object takes about 16 bytes versus 4 bytes for an int, and the extra objects add work for the garbage collector. In everyday code you won't notice it, but in hot loops and when processing large arrays of numbers the difference can be several-fold.

A typical mistake is declaring an accumulator as a wrapper:

public class BoxingInLoop {
    public static void main(String[] args) {
        Integer boxedSum = 0;
        for (int i = 0; i < 10_000; i++) {
            boxedSum += i;   // boxedSum = Integer.valueOf(boxedSum.intValue() + i)
        }

        int sum = 0;
        for (int i = 0; i < 10_000; i++) {
            sum += i;        // plain int arithmetic
        }

        System.out.println(boxedSum + " " + sum);
    }
}

Output:

49995000 49995000

The results are identical, but the first loop created 9,984 Integer objects: the running sum leaves the cache range (goes above 127) on the 17th iteration, and from then on every += boxes a new value. The JIT compiler can sometimes eliminate such allocations (escape analysis), but you cannot count on it.

The same happens in the Stream API when you work with a Stream<Integer> instead of a primitive stream:

List<Integer> list = List.of(1, 2, 3, 4);
int s1 = list.stream().reduce(0, Integer::sum);            // unboxing and boxing at every step
int s2 = list.stream().mapToInt(Integer::intValue).sum();  // IntStream works with int
System.out.println(s1 + " " + s2);                         // 10 10

Recommendations:

  • declare loop counters, sums and other accumulators as primitives (int, long, double);
  • for large sets of numbers, use int[] arrays and the IntStream, LongStream, DoubleStream streams instead of List<Integer> and Stream<Integer>;
  • don't mix wrappers and primitives in one expression without a reason: every mix means unboxing, and assigning the result to a wrapper means another boxing;
  • keep wrappers where they are really needed: collections, generics, and null as «no data».

Frequently Asked Questions

What is the difference between boxing and autoboxing?

Boxing is the conversion of a primitive into a wrapper object itself, and you can do it explicitly: Integer.valueOf(5). Autoboxing is the same operation inserted by the compiler for you: Integer x = 5;. Likewise, unboxing is x.intValue(), and automatic unboxing is an implicit call to that method. In everyday speech, «boxing» and «unboxing» usually mean the automatic variant.

Which Java version introduced autoboxing?

Java 5 (J2SE 5.0, 2004), together with generics, enum, varargs and the for-each loop. Autoboxing needs no new JVM features: the compiler turns it into ordinary valueOf() and xxxValue() calls that are visible in the bytecode.

Why can't an int[] be assigned to an Integer[] variable?

Autoboxing works only for individual values, not for arrays: int[] and Integer[] are different, unrelated types, and the compiler reports incompatible types. You can convert an array with a stream: Arrays.stream(arr).boxed().toArray(Integer[]::new). For the same reason Arrays.asList(new int[]{1, 2, 3}) returns a List<int[]> with a single element, not a List<Integer>.

Can I use an Integer in a switch, and what happens with null?

Yes, switch accepts Byte, Short, Character and Integer: the value is unboxed before a branch is chosen. If the variable is null, a classic switch throws a NullPointerException. Since Java 21 (JEP 441) you can add a case null branch and handle the missing value without an exception.

Comments

Please log in or register to have a possibility to add comment.