skip to content

Why can == sometimes return true and sometimes false when comparing two Integer objects with the same value?

level: seniorimportance: should knowfreq 55%

answer

  1. Integer is an object -> == is reference identity
  2. Cache -128..127: same-value boxes are == in range, not outside
  3. Compare wrappers with .equals() or unbox to int
  4. Wrapper == primitive triggers unboxing (value compare, but NPE if null)
  5. Float/Double never cache; Long/Short/Byte/Char do

basics

~20 s

Integer is an object, so == compares references, not the number. Java caches small boxed values (-128 to 127), so two boxed ints in that range can share one object and be == ; outside that range they are different objects and == is false. Always use .equals() or unbox to int.

solid answer

~40 s

Integer is a wrapper object, so == compares object references, not the numeric value. The surprise comes from the integer cache: autoboxing via Integer.valueOf returns cached, shared instances for values in -128..127 (configurable upper bound), so two boxed values in that range are the same object and == is true. Box a value outside that range (e.g. 1000) and each box is a fresh object, so == is false even though the numbers are equal. The fix is to compare with .equals(), or unbox to a primitive int (a == b where at least one side is an int triggers unboxing and value comparison). This also intersects with NullPointerException risk: unboxing a null Integer throws, so == against a primitive can blow up. Treat == on wrappers as a code smell.

code

java · 12 lines
java
Integer a = 127, b = 127;
Integer c = 128, d = 128;

System.out.println(a == b);        // true:  cached (-128..127)
System.out.println(c == d);        // false: outside cache, distinct objects
System.out.println(c.equals(d));   // true:  value comparison

int primitive = 128;
System.out.println(c == primitive);// true:  c is unboxed to int

Integer n = null;
// System.out.println(n == primitive); // NullPointerException on unboxing

go deeper

for a junior

May only know to use .equals() for objects; the cache nuance is above this level.

for a middle

Knows Integer == compares references and that .equals() or unboxing is the fix.

for a senior

Explains the -128..127 cache, the wrapper-vs-primitive unboxing behavior, and the null-unboxing NPE.

for a principal

Discusses why autoboxing in hot paths or as map keys is a design risk, the JVM cache tunable, and steering APIs toward primitives or value-based comparisons.

## The setup: wrappers and autoboxing Each primitive has a **wrapper class** — `int`/`Integer`, `long`/`Long`, `boolean`/`Boolean`, etc. A wrapper is a full **object** on the heap. **Autoboxing** is the compiler automatically converting a primitive to its wrapper (`Integer x = 5;` really calls `Integer.valueOf(5)`), and **unboxing** is the reverse (`int y = x;` calls `x.intValue()`). ## Why == surprises you Because an `Integer` is an object, `==` compares **references** (identity), not the wrapped number. So whether `==` is true depends on whether the two boxes are the *same object*. ## The Integer cache The JLS requires `Integer.valueOf` to **cache** boxed values in the range **-128 to 127** (the upper bound is tunable via `-XX:AutoBoxCacheMax` / `java.lang.Integer.IntegerCache.high`). Within that range, autoboxing the same value returns the **same shared instance**: ```java Integer a = 100, b = 100; System.out.println(a == b); // true — both from the cache Integer c = 1000, d = 1000; System.out.println(c == d); // false — two separate objects ``` This is why the *same code pattern* flips behavior depending on the magnitude — a notorious gotcha. `Boolean`, `Byte`, `Character` (0–127), `Short` and `Long` (-128..127) cache too; `Float`/`Double` never cache. ## The correct ways to compare 1. **`.equals()`** — `a.equals(b)` compares values regardless of caching. 2. **Unbox to primitive** — if at least one operand is a primitive `int`, the other is unboxed and the comparison becomes a value comparison: ```java Integer c = 1000; int p = 1000; System.out.println(c == p); // true — c is unboxed to int ``` 3. **`Objects.equals` / `Integer.compare`** for null-tolerant or relational needs. ## The hidden NullPointerException Unboxing a `null` wrapper throws NPE. So: ```java Integer maybe = null; int p = 5; // maybe == p; // unboxes maybe -> NullPointerException ``` A `==` between a wrapper and a primitive silently introduces an unboxing that can NPE — another reason to be deliberate. ## Relational operators on wrappers `<`, `<=`, `>`, `>=` on `Integer` operands **unbox** both sides and compare values (there is no identity option for relational), which is usually what you want — but it shares the same null-unboxing NPE risk. ## Why it matters This is a senior-level trap because it passes naive tests (small literals land in the cache) and fails in production (real-world values exceed 127). The rule: never use `==` to compare wrapper *values* — use `.equals()` or work in primitives.

  • What is the guaranteed Integer cache range and can it change?
    -128 to 127 is guaranteed by the JLS. The upper bound can be raised via -XX:AutoBoxCacheMax or the java.lang.Integer.IntegerCache.high property, but you should never rely on caching for correctness.
  • How can a simple Integer == comparison cause a NullPointerException?
    If one side is a primitive int, the Integer is unboxed via intValue(). If that Integer is null, unboxing throws NPE before any comparison happens.

saying these in an interview costs you the question

  • Concluding Integer == always works because it worked for small numbers
  • Forgetting that unboxing a null wrapper throws NPE
  • Thinking the cache range is part of the language guarantee for all values (only -128..127 is guaranteed)

context