Wrapper Classes and Autoboxing

Understand Integer, Long and the other wrappers, when Java boxes and unboxes for you, and the == and null traps that follow.

SyntaxInteger boxed = 42; // boxing: Integer.valueOf(42) int value = boxed; // unboxing: boxed.intValue() int n = Integer.parseInt("8080"); boolean same = a.equals(b); // never a == b on wrappers

Each primitive has a wrapper class in java.lang: Integer, Long, Double, Float, Short, Byte, Character and Boolean. A wrapper is an object holding one primitive value. You need them because generics only work with reference types: there is no List<int>, only List<Integer>.

Autoboxing and unboxing

The compiler converts between the two for you. Putting an int where an Integer is expected is boxing; it calls Integer.valueOf. Using an Integer where an int is expected is unboxing; it calls intValue(). That makes list.add(200) and int code = list.get(0) work without ceremony.

Trap one: == compares references

On two wrappers, == asks whether they are the same object. Java caches boxed values from -128 to 127, so == happens to work for small numbers in tests and then fails for real IDs like 1000 in production. Compare wrappers with equals, or unbox one side first. Objects.equals(a, b) also handles null. Note that equals checks the type as well: with Long id = 5L;, id.equals(5) is false because 5 boxes to an Integer. Compare a Long ID with 5L.

Trap two: unboxing null throws

A wrapper can be null; a primitive cannot. Unboxing null throws NullPointerException, and the line often looks harmless: int stock = inventory.get(sku); fails when the key is missing because Map.get returned null. Use getOrDefault, check for null, or keep the value as an Integer.

When null is useful

That same null is the reason to choose a wrapper on purpose. In a PATCH request DTO, Integer age can tell "field not sent" (null) apart from "set to 0". A nullable database column maps naturally to a wrapper, while a NOT NULL column fits a primitive. Use primitives for plain local arithmetic and counters: they are smaller, faster and cannot be null.

Parsing and converting

Integer.parseInt(s) returns an int; Integer.valueOf(s) returns an Integer. Both throw NumberFormatException for anything that is not a plain number, including surrounding spaces, so query parameters and headers need a try/catch that turns the failure into a 400 response. Boolean.parseBoolean never throws: anything other than "true" (in any case) is false. The wrapper constructors such as new Integer(5) are deprecated for removal; use valueOf or let boxing do it.

Boxing in a hot loop creates objects. Summing a List<Integer> into a Long accumulator boxes on every iteration; use a primitive accumulator.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Keep a List<Long> of user IDs loaded from a query, since collections only hold objects.
  • Model optional numeric fields in a PATCH DTO as Integer so null means the client left them out.
  • Parse a ?limit= query parameter with Integer.parseInt and answer 400 on NumberFormatException.

More examples

== on wrappers works until it does not

Values from -128 to 127 come from a cache, so == finds the same object. For 1000 each boxing creates a new object, and only equals or a numeric comparison gives the right answer.

Example · java
void main() {
    Integer a = 127, b = 127;
    Integer c = 1000, d = 1000;
    IO.println(a == b);               // same cached object
    IO.println(c == d);               // two different objects
    IO.println(c.equals(d));
    IO.println(Objects.equals(c, d));
    IO.println(c.intValue() == d);    // one side unboxed: numeric comparison
}
// Output:
// true
// false
// true
// true
// true

Unboxing null from a Map

The assignment to an int forces unboxing of the null that Map.get returns for a missing key. getOrDefault supplies a value, and keeping the Integer lets you treat missing differently from zero.

Example · java
void main() {
    Map<String, Integer> stock = new HashMap<>();
    stock.put("SKU-1", 3);

    try {
        int missing = stock.get("SKU-2");   // get returns null, unboxing throws
        IO.println(missing);
    } catch (NullPointerException e) {
        IO.println("NullPointerException while unboxing");
    }
    int safe = stock.getOrDefault("SKU-2", 0);
    Integer maybe = stock.get("SKU-2");     // keep it boxed to test for null
    IO.println(safe + " " + (maybe == null ? "unknown" : maybe));
}
// Output:
// NullPointerException while unboxing
// 0 unknown

Nullable fields in a PATCH request and parsing a query parameter

An Integer can say the client did not send the value, which an int cannot. parseLimit keeps null for a missing parameter and turns NumberFormatException into an exception the handler maps to a 400 instead of a 500.

Example · java
record UserPatch(String name, Integer age) {}

Integer parseLimit(String raw) {          // null means "not sent"
    if (raw == null) return null;
    try {
        return Integer.valueOf(raw);
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("limit is not a number: " + raw);
    }
}

void main() {
    var patch = new UserPatch("Ada", null);
    IO.println(patch.age() == null ? "age not sent, keep current value" : "set age to " + patch.age());

    for (String raw : new String[] {"25", null, "25a"}) {
        try {
            Integer limit = parseLimit(raw);
            IO.println(limit == null ? "limit not sent, use default" : "limit=" + limit);
        } catch (IllegalArgumentException e) {
            IO.println("400 Bad Request: " + e.getMessage());
        }
    }
}
// Output:
// age not sent, keep current value
// limit=25
// limit not sent, use default
// 400 Bad Request: limit is not a number: 25a

Discussion

  • Be the first to comment on this lesson.