Skip to main content

Primitives vs. Objects

Java has two kinds of values — the eight primitive types that live on the stack and wrapper objects that live on the heap — and understanding the boundary between them prevents a whole class of subtle bugs.

What Problem Does It Solve?​

Early languages like C treat everything as raw memory, making programs fast but dangerous (null pointer dereferences, buffer overflows). Java's designers wanted safety and interoperability with the object-oriented Collections framework, but they also wanted performance for numeric computation. The solution was a split: primitives for raw speed and wrapper objects for type-system compatibility. The cost? A boundary between the two that occasionally surprises developers through autoboxing, null references, and unexpected performance overhead.

The Eight Primitive Types​

Java defines exactly eight primitive types. They are not objects — they have no methods, no null value, and no identity.

PrimitiveWrapper ClassSizeDefault ValueRange
byteByte8-bit0−128 to 127
shortShort16-bit0−32,768 to 32,767
intInteger32-bit0−2³¹ to 2³¹−1
longLong64-bit0L−2⁶³ to 2⁶³−1
floatFloat32-bit0.0f~±3.4×10³⁸
doubleDouble64-bit0.0d~±1.8×10³⁰⁸
charCharacter16-bit'\u0000'0 to 65,535 (Unicode)
booleanBooleanJVM-dependentfalsetrue / false

Analogy — Cash vs. Cheque​

Think of a primitive as cash in your pocket — it's the actual value, instantly accessible, with no indirection. A wrapper object is a cheque — it's a reference to a value held somewhere else (the heap). Giving someone a cheque means there's an extra step to get the money, and the cheque itself can be null (dishonoured). Cash can never be null.

How It Works​

Memory Layout​

Stack Frame                   Heap
┌──────────────┐ ┌─────────────────────┐
│ int x = 42 │ 42 stored │ │
│ (4 bytes) │ directly ──►│ (no heap entry) │
├──────────────┤ ├─────────────────────┤
│ Integer y │ reference──►│ Integer object │
│ (ref, 4/8B) │ │ { value: 42 } │
└──────────────┘ └─────────────────────┘
  • Primitive int x = 42 — the value 42 is stored directly in the stack frame.
  • Object Integer y = 42 — the stack holds a reference (pointer) to an Integer object on the heap.

Autoboxing — Automatic Wrapping​

The compiler silently converts a primitive to its wrapper when the context requires an object:

List<Integer> list = new ArrayList<>();
list.add(42); // ← autoboxing: compiler inserts Integer.valueOf(42)

This is purely a compile-time convenience. At bytecode level it becomes Integer.valueOf(42).

Unboxing — Automatic Unwrapping​

The reverse: a wrapper is silently converted to a primitive when a numeric context requires it:

Integer wrapped = Integer.valueOf(100);
int result = wrapped + 5; // ← unboxing: compiler inserts wrapped.intValue()

Autoboxing and unboxing are compiler-inserted conversions at the boundary between primitive and object contexts.

Integer Cache — The Sneaky Optimisation​

Integer.valueOf(n) caches instances for values in the range −128 to 127. This makes identity comparisons (==) misleading:

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true — same cached object

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false — different heap objects!
System.out.println(c.equals(d)); // true — correct equality check

Code Examples​

Practical Demo

See the Primitives vs. Objects Demo for step-by-step runnable examples covering the Integer cache, unboxing NPEs, and performance benchmarks.

Null NPE via Unboxing​

Integer count = null;          // ← valid; objects can be null
int total = count + 1; // ← NullPointerException at runtime!
// unboxing calls count.intValue() on null

Performance — Summing Primitives vs. Wrappers​

// Fast — no heap allocation, no boxing
long sumPrimitive = 0;
for (int i = 0; i < 1_000_000; i++) {
sumPrimitive += i; // ← pure stack arithmetic
}

// Slow — 1 million Integer allocations + GC pressure
Long sumWrapped = 0L; // ← already wrong type; forces boxing every iteration
for (int i = 0; i < 1_000_000; i++) {
sumWrapped += i; // ← unbox sumWrapped, add, re-box result
}

Comparing Wrapper Objects Correctly​

Integer x = 200;
Integer y = 200;

// Wrong — tests reference identity, not value
if (x == y) { ... } // false for values outside [-128, 127]

// Correct — tests value equality
if (x.equals(y)) { ... } // true
if (Objects.equals(x, y)) { ... } // null-safe version ← prefer this

Using Optional Instead of Nullable Wrappers​

// Avoid: Integer can be null, causing hidden NPEs
Integer findAge(String name) { ... }

// Better: communicate absence explicitly
OptionalInt findAge(String name) { ... }

Best Practices​

  • Use primitives (int, long, double) for local variables and fields where null is not a valid state.
  • Use wrapper types only when required by an API (List<Integer>, Optional<Integer>, generics).
  • Always use .equals() (or Objects.equals()) to compare wrapper objects — never ==.
  • Avoid mutable Long/Integer fields in hot loops; reassignment triggers boxing each time.
  • Use int[] over List<Integer> for large numeric datasets where GC pressure matters.
  • Be explicit about null: if a method can return "no value," use OptionalInt or document the null contract.

Common Pitfalls​

  1. Unboxing a null wrapper — the most common autoboxing NPE. Guard with a null check or Objects.requireNonNullElse.
  2. Using == on cached Integers — works for [-128, 127] but silently fails outside that range.
  3. Long sum = 0 in a loop — boxes on every iteration; use long sum = 0 instead.
  4. Passing null to a primitive parameter — Java will not autobox null; the compiler rejects it at compile time but a nullable wrapper variable being passed can fool you.
  5. switch on a nullable Integer — the switch statement unboxes the expression, yielding NPE if it's null.

Interview Questions​

Beginner​

Q: What are the eight primitive types in Java? A: byte, short, int, long, float, double, char, and boolean. They are stored by value, not by reference, and cannot be null.

Q: What is autoboxing? A: The compiler's automatic conversion of a primitive to its wrapper class when an object is required — for example, list.add(5) becomes list.add(Integer.valueOf(5)).

Intermediate​

Q: Why can Integer a = 127; Integer b = 127; a == b be true while Integer a = 128; Integer b = 128; a == b is false? A: Integer.valueOf() caches instances for −128 to 127 (the JVM integer cache). Within that range, the two variables point to the same cached object. Outside it, new heap objects are allocated, so == compares different references and returns false. Always use .equals() for wrapper equality.

Q: Where does an int variable live vs. an Integer variable? A: An int local variable lives on the JVM stack frame — the value is stored directly. An Integer variable stores a reference on the stack, but the actual Integer object lives on the heap, requiring an extra memory access and contributing to GC pressure.

Advanced​

Q: Can autoboxing cause performance issues in production code? Give an example. A: Yes. A classic case is using Long instead of long as an accumulator in a tight loop. Each += operation unboxes the current Long, performs the addition, then creates a new Long object (boxing), discarding the old one. This generates millions of short-lived heap objects, increasing GC pause frequency. The fix is to use the primitive long type and only box the final result if needed.

Q: What does the JVM specification say about boolean size? A: The JVM spec does not mandate a specific size for boolean. In practice, it is typically stored as a 32-bit int on the stack (for local variables) or as a single byte in arrays (boolean[]). This is an implementation detail — the language spec says boolean has values true and false, nothing more.

Further Reading​

  • Variables and Data Types — introduces primitive syntax and default values; start here if you're new to Java types.
  • Type Conversion — widening and narrowing conversions between primitive types; closely related to autoboxing rules.
  • Generics — generics work only with reference types, which is why wrapper classes exist; understanding primitives makes the limitation intuitive.