Recommended resources
The freely available resources this lesson will direct you to.
Welcome back. Previously, you traced how Java evaluates numeric expressions: operands are promoted before arithmetic, integral overflow wraps silently, and assignment happens only after an expression has a result. This lesson adds the object boundary to that model.
Java has primitive values such as int and double, but it also has object wrappers such as Integer and Double. Autoboxing and unboxing make crossing that boundary look effortless—sometimes too effortless. For interviews, the important questions are: when does Java create or use a wrapper, why can Integer == Integer be surprising, how does null cause failures, and when do wrappers create meaningful performance costs?
1. Primitives and wrappers: two forms of the same value
Each primitive has a corresponding wrapper class:
| Primitive | Wrapper |
|---|---|
boolean |
Boolean |
byte |
Byte |
short |
Short |
char |
Character |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
A primitive variable contains its value directly. A wrapper variable contains a reference to an immutable object holding a value.
int primitiveCount = 42;
Integer wrappedCount = 42;
The second assignment uses autoboxing: Java converts the primitive int value into an Integer reference. A useful conceptual expansion is:
Integer wrappedCount = Integer.valueOf(42);
The reverse conversion is unboxing:
Integer wrappedCount = 42;
int primitiveCount = wrappedCount;
Conceptually, the final line behaves like:
int primitiveCount = wrappedCount.intValue();
The distinction matters because an Integer is not an int with a capital letter. It is an object reference:
- It can be
null; anintcannot. - It has an identity, even though wrapper equality should normally be based on value.
- It works where an
Objectis required. - It has object-related memory and allocation costs.
- It is immutable: changing an
Integervariable means replacing its reference with another wrapper object, not changing an existing object.
Java needs wrappers because APIs based on objects cannot directly work with primitives. For example, generic collections hold references:
List<Integer> ports = new ArrayList<>();
ports.add(8080); // boxes int 8080 into Integer
int port = ports.get(0); // retrieves Integer, then unboxes it
Java generics do not support List<int>. Later, you will examine collections in depth; for now, recognize that List<Integer> necessarily stores object references, not raw int values.
For a concise visual and spoken overview, watch the following segments.
What is Autoboxing and Unboxing in Java? - 045
Watch “What is Autoboxing and Unboxing in Java?” from Deege. It introduces wrapper classes and then connects the automatic conversions to identity, nullability, and performance.
Watch wrapper classes for why Java retains wrappers alongside primitives. Then watch automatic conversion to see both boxing and unboxing in source code. Finish with the pitfalls, focusing on why wrapper identity, null, and repeated conversions matter.
Where conversions commonly happen
Autoboxing is common when:
Integer id = 17; // assignment
sendToService(17); // method expects Integer
numbers.add(17); // collection expects Integer
return 17; // method returns Integer
Unboxing is common when a primitive is required:
Integer quantity = 5;
int rawQuantity = quantity; // assignment to int
int next = quantity + 1; // arithmetic
boolean positive = quantity > 0; // comparison
The numeric expression quantity + 1 is particularly important. Java first unboxes quantity to int, then applies the numeric-promotion rules from the previous lesson.
One boundary that catches candidates:
Long id = 1L; // Valid: long literal is boxed into Long
// Long id = 1; // Compile-time error
1 is an int literal. Java does not generally perform primitive widening from int to long and then box it into Long in this assignment. Use 1L when the target wrapper is Long.
2. The specification rules worth knowing
Autoboxing is convenient syntax, but its behavior is defined by the language specification rather than being an informal compiler trick. Read the focused parts of the Java Language Specification now. They are dense, so aim to extract the rules rather than memorize its wording.
Chapter 5. Conversions and Contexts
Read the relevant parts of Oracle’s Java Language Specification. This is the authoritative source for the primitive-to-wrapper mappings, the limited identity guarantees for boxed values, null unboxing, and unboxing during numeric promotion.
In Section 5.1.7, “Boxing Conversion,” read from the boxing definition through the discussion of the required cached values. Focus on the fact that the specification guarantees identity only for particular boxed constant values, not for arbitrary wrapper objects. Next, in Section 5.1.8, “Unboxing Conversion,” read from the unboxing definition through the final bullet stating the result of unboxing null. Finally, in Section 5.6, “Numeric Contexts and Numeric Promotion,” read the opening rules from numeric promotion. Notice that a reference value in a numeric context is unboxed before primitive numeric promotion proceeds.
A practical interview answer should be simpler than the specification:
Autoboxing converts a primitive to its matching wrapper when a reference is required. Unboxing extracts the primitive when Java needs a primitive value. Unboxing is not null-safe, and boxed objects should be compared by value rather than identity.
3. Wrapper identity, value equality, and caches
This is one of Java’s classic interview traps:
Integer first = 100;
Integer second = 100;
System.out.println(first == second); // true
System.out.println(first.equals(second)); // true
At first glance, this may suggest that == compares Integer values. It does not.
When both operands are references, == asks whether they refer to the same object. The result is true here because Java reuses a cached wrapper for this small boxed integer value.
Now consider:
Integer first = 200;
Integer second = 200;
System.out.println(first.equals(second)); // true
System.out.println(first == second); // Do not rely on either result
equals() compares the contained integer values, so it is reliably true. In contrast, first == second is an identity test. Outside the required cache range, Java provides no general identity guarantee. Typical current JDK behavior makes this expression false, but correct code—and a precise interview answer—must not depend on it.
The required cache range
For boxing constant expressions, Java guarantees identical references for:
| Wrapper category | Guaranteed shared boxed values |
|---|---|
Boolean |
true, false |
Byte |
every byte value |
Character |
'\u0000' through '\u007f' |
Short, Integer, Long |
through |
The commonly remembered rule is: Integer values from -128 through 127 are cached. That rule explains many snippets, but it should not become a reason to use ==.
Implementations may cache additional values, and the JLS deliberately permits implementation flexibility outside the guaranteed values. Therefore, cache behavior is an optimization—not a value-comparison technique.
Interview Question : Compare Two Integer Numbers (Integer Caching) in #Java
Watch “Interview Question: Compare Two Integer Numbers (Integer Caching) in Java” from Naveen AutomationLabs for a short demonstration of the cache surprise.
Watch the first comparison, where two boxed 100 values appear identical under ==. Then watch the cache contrast, which changes the number and explains the usual -128 through 127 range. Treat the video’s output examples as cache behavior, not as permission to compare wrappers with ==.
== changes meaning when a primitive is involved
This variation behaves differently:
Integer wrapped = 1_000;
int primitive = 1_000;
System.out.println(wrapped == primitive); // true
Because one side is primitive, Java unboxes wrapped and performs a primitive value comparison. Conceptually:
System.out.println(wrapped.intValue() == primitive);
That result is value-based—but it has a risk: if wrapped is null, the comparison throws NullPointerException.
Safe comparison rules
Use these rules in production and in interviews:
Integer left = 200;
Integer right = 200;
boolean sameValue = left.equals(right);
boolean sameValueNullSafe = Objects.equals(left, right);
- Use
==for two primitives. - Use
.equals()for two known non-null wrappers. - Use
Objects.equals(a, b)when either wrapper could benull. - Avoid relying on wrapper caches.
- Be aware that
Integer.equals(Long)isfalse, even if both contain the mathematical value1, because they are different wrapper types.
4. Null unboxing: a compact source of production failures
A primitive always has a value; a wrapper reference may be absent:
int primitive = 0;
Integer wrapper = null;
The error happens not when you assign null to the wrapper, but when Java later needs its primitive value.
Integer retries = null;
// int count = retries; // Throws NullPointerException
The conceptual conversion is:
int count = retries.intValue();
Calling an instance method through a null reference throws NullPointerException.
This is especially common at backend boundaries: database records, JSON request models, ORM entities, configuration maps, and caches often use wrappers because a numeric field may be absent.
Map<String, Integer> limits = loadLimits();
int retryLimit = limits.get("retry");
The code compiles, but Map.get() returns null when there is no mapping. Assigning that null to int triggers unboxing and fails.
Handle absence explicitly:
Integer configuredLimit = limits.get("retry");
int retryLimit;
if (configuredLimit == null) {
retryLimit = 3;
} else {
retryLimit = configuredLimit;
}
For a genuinely appropriate default, a compact form is reasonable:
int retryLimit = Objects.requireNonNullElse(
limits.get("retry"),
3
);
However, a default is a business decision, not just an NPE fix. For example, an absent credit limit might mean “unknown” or “reject the request,” not zero.
Boolean creates a similar trap:
Boolean approved = null;
// if (approved) { } // NullPointerException
If the intended meaning is “perform an action only when explicitly true,” write:
if (Boolean.TRUE.equals(approved)) {
// Runs only for Boolean.TRUE
}
Do not use this pattern when null represents an important third state that needs its own handling.
5. Why repeated boxing can be expensive
The visual below summarizes useful wrapper habits. Its most important ideas are to handle null deliberately, compare values correctly, and avoid unnecessary conversions in performance-sensitive code.

A wrapper object is more expensive than a primitive value because it involves an object reference and, when an object is not reused or optimized away, object allocation and later garbage collection. An Integer[] or List<Integer> also stores references to separate wrapper objects rather than densely packed primitive integers.
Consider this counter:
Integer total = 0;
for (int i = 0; i < 1_000_000; i++) {
total++;
}
total++ cannot mutate an Integer; wrapper objects are immutable. Conceptually, each iteration does the following:
- Unboxes
totalto anint. - Adds one using primitive arithmetic.
- Boxes the result back into an
Integer. - Assigns the new reference to
total.
Use a primitive where object semantics are not required:
int total = 0;
for (int i = 0; i < 1_000_000; i++) {
total++;
}
This does not mean “wrappers are always bad.” Integer is correct when an API needs a reference or a value genuinely can be absent. The objective is to keep that object boundary at the edge of the operation rather than repeatedly crossing it inside a hot loop.
Common sources of accidental boxing
| Situation | What happens | Prefer when performance matters |
|---|---|---|
List<Integer> |
Stores object references and boxes inserted int values |
int[] when size and access pattern fit |
Stream<Integer> |
May box values across pipeline stages | IntStream, LongStream, or DoubleStream |
Optional<Integer> |
The contained numeric value is boxed | OptionalInt, OptionalLong, or OptionalDouble |
Object... varargs |
Primitives passed to it must be boxed; the call also creates an array | A primitive-specific method where justified |
Integer total++ |
Repeated unboxing and reboxing | Primitive local variable or appropriate numeric accumulator |
The JVM can sometimes eliminate temporary wrapper allocations using JIT optimizations such as escape analysis. That is valuable, but it is not a contract your code should depend on. Values stored in collections or passed widely through APIs generally cannot be treated like simple local primitive values.
For ordinary application code, favor clarity first. Investigate boxing when profiling shows allocation pressure, garbage-collection activity, or latency in a hot path. Java Flight Recorder and a proper benchmark tool such as JMH are appropriate tools for confirming a suspected cost; timing one quick loop with System.nanoTime() is not reliable evidence.
6. Interview-ready answers and a prediction routine
Interview Q: What is the difference between autoboxing and unboxing?
Answer: Autoboxing converts a primitive to its corresponding wrapper type, such as int to Integer, when a reference is required. Unboxing converts a wrapper back to its primitive value, such as when an Integer is used in arithmetic. Both conversions can be inserted implicitly by the compiler.
Interview Q: Why does Integer a = 100; Integer b = 100; a == b often return true?
Answer: == compares object identity when both operands are Integer references. Java guarantees reuse for boxed integer constants from through , so both references point to the same cached object in this case. This is cache behavior, not value equality; use equals() or Objects.equals() to compare wrapper values.
Interview Q: Is Integer a = 200; Integer b = 200; a == b guaranteed to be false?
Answer: No. Outside the required cache range, Java gives no general identity guarantee. A typical runtime creates distinct objects, but code must not rely on either true or false. Use a.equals(b) for numeric value equality.
Interview Q: When can autounboxing throw NullPointerException?
Answer: Whenever Java needs a primitive but the wrapper reference is null. Examples include assigning Integer to int, using a nullable Integer in arithmetic, comparing it to a primitive with ==, or using a nullable Boolean as an if condition.
Interview Q: Does == always compare values if one operand is an Integer?
Answer: No. It compares values only when Java unboxes because a primitive operand requires it. For Integer == Integer, it compares references. If a wrapper is nullable, relying on unboxing for comparison can also throw NullPointerException.
Interview Q: What are the performance implications of autoboxing?
Answer: Boxing can create wrapper objects, increase memory use, reduce locality in collections, and create garbage-collection pressure. Repeated boxing and unboxing also adds conversion work. The impact is usually insignificant for small amounts of business logic, but it can matter in high-volume loops, numeric pipelines, and allocation-heavy backend workloads.
When predicting any wrapper snippet, follow this short routine:
- Identify whether each variable is primitive or a wrapper reference.
- Mark every point where a reference context requires boxing or a primitive context requires unboxing.
- If comparing wrappers, determine whether
==is testing identity or unboxing is forcing a value comparison. - Check every possible unboxing operation for
null. - For frequently repeated code, ask whether a primitive representation can remain in use until the API boundary.
Key takeaways
- Autoboxing converts a primitive to its matching wrapper; unboxing extracts a primitive from a wrapper.
- Wrappers are immutable reference objects that can be
null; primitives are values and cannot benull. - Wrapper-to-wrapper
==compares identity, not numeric value. Prefer.equals()orObjects.equals(). - Small boxed integral values may be shared through caches, but cache behavior is never a reason to use
==for value comparison. - Unboxing
nullthrowsNullPointerException, commonly when values come from maps, configuration, persistence, or request data. - Boxing is often necessary for generic and object-oriented APIs, but repeated conversion in hot numeric code can cause allocation and garbage-collection overhead.
Next, you will look at var: Java’s local-variable type inference. The central theme carries forward—var saves typing, but it does not remove static types, so an inferred Integer remains very different from an inferred int.