Back to Primitive Types & Wrappers
Q5Question 5 of 11

Why is Integer 100 == 100 true but Integer 1000 == 1000 false?

Key Concepts
  • Integer.valueOf() caches objects for -128 to 127 – two Integers in range share the same object
  • == on objects compares references (identity), not values – it accidentally works inside the cache
  • Outside the cache, valueOf() creates new objects each time – == returns false even for equal values
  • .equals() compares the actual int values and always returns the correct answer
  • The cache upper bound is tunable via -XX:AutoBoxCacheMax or -Djava.lang.Integer.IntegerCache.high
  • Same trap exists for Long (-128 to 127), but Float and Double are never cached

Definition

Integer.valueOf() caches objects for -128 to 127. Inside the cache, == returns true because both references point to the same shared object. Outside, == returns false because each call creates a new object. Use .equals() to compare values.

The behavior comes from three facts:

  • Autoboxing calls valueOf(). Writing Integer a = 100 compiles to Integer a = Integer.valueOf(100).
  • valueOf() caches -128 to 127. For values in this range, it returns the same pre-created object every time. For values outside, it creates a new object.
  • == compares references, not values. Two references to the same cached object compare as equal. Two different objects with the same value compare as not equal.

The fix is simple: always use .equals() or unbox to primitives.

The bank keeps a tray of 256 pre-printed token cards numbered -128 to 127. When two customers ask for token 100, the clerk hands out the same card from the tray. Holding them side by side, they are the same physical card, so asking "is this the identical card?" (==) says yes. For token 1000, no pre-printed card exists. The clerk prints a fresh one each time. Now two cards say 1000 but they are different pieces of paper, so "is this the identical card?" says no. Asking "do both cards show the same amount?" (.equals()) always gives the right answer.

Understanding the Concepts

What exactly happens with Integer 100 == 100?

JAVA
Integer a = 100;
Integer b = 100;
System.out.println(a == b);       // true
System.out.println(a.equals(b));  // true

The compiler converts Integer a = 100 into Integer a = Integer.valueOf(100). Because 100 is inside the cache range (-128 to 127), valueOf returns a shared cached object. Both a and b receive a reference to that same object. Since == compares references, and both point to the same object, it returns true.

What exactly happens with Integer 1000 == 1000?

JAVA
Integer a = 1000;
Integer b = 1000;
System.out.println(a == b);       // false
System.out.println(a.equals(b));  // true

Because 1000 is outside the cache, valueOf creates a new Integer object each time. a and b point to two different objects. Since == compares references, and the references differ, it returns false. But .equals() compares the int values inside the objects, so it returns true.

What does Integer.valueOf look like inside the JDK?

Here is a simplified version of the actual JDK source code:

JAVA
// Simplified from java.lang.Integer (OpenJDK)
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high) {
        return IntegerCache.cache[i + (-IntegerCache.low)];
    }
    return new Integer(i);
}

private static class IntegerCache {
    static final int low = -128;
    static final int high;  // default 127, tunable
    static final Integer[] cache;

    static {
        // Read the tunable high from system property
        int h = 127;
        String prop = System.getProperty("java.lang.Integer.IntegerCache.high");
        if (prop != null) {
            h = Math.max(Integer.parseInt(prop), 127);
        }
        high = h;

        // Pre-create Integer objects for the entire range
        cache = new Integer[(high - low) + 1];
        for (int k = 0; k < cache.length; k++) {
            cache[k] = new Integer(low + k);
        }
    }
}

The cache is a simple array initialized once when the Integer class loads. For values inside the range, valueOf returns cache[i + 128]. For values outside, it calls new Integer(i).

Where exactly is the boundary?

JAVA
Integer a127 = 127, b127 = 127;
Integer a128 = 128, b128 = 128;

System.out.println("127 == 127: " + (a127 == b127));  // true  (last cached value)
System.out.println("128 == 128: " + (a128 == b128));  // false (first uncached value)

Integer aNeg = -128, bNeg = -128;
Integer aNeg2 = -129, bNeg2 = -129;

System.out.println("-128 == -128: " + (aNeg == bNeg));    // true  (last cached negative)
System.out.println("-129 == -129: " + (aNeg2 == bNeg2));  // false (outside cache)

The cache covers exactly -128 to 127 (256 values). The boundary is sharp: 127 is cached, 128 is not.

Does the same trap exist for Long, Short, and other wrappers?

Yes. The same cache behavior exists for Long, Short, Byte, and Character.

JAVA
Long la = 100L, lb = 100L;
Long lx = 1000L, ly = 1000L;
System.out.println("Long 100 == 100: " + (la == lb));    // true (cached)
System.out.println("Long 1000 == 1000: " + (lx == ly));  // false (not cached)

// Byte is ALWAYS cached (entire range -128 to 127 fits)
Byte ba = 42, bb = 42;
System.out.println("Byte 42 == 42: " + (ba == bb));  // true (always cached)

// Double is NEVER cached
Double da = 3.14, db = 3.14;
System.out.println("Double 3.14 == 3.14: " + (da == db));  // false (never cached)
System.out.println("Double 3.14.equals: " + da.equals(db)); // true

// Boolean is always cached (only two values)
Boolean ta = true, tb = true;
System.out.println("Boolean true == true: " + (ta == tb));  // true (always cached)

How should you compare wrapper objects?

Method 1: .equals() (recommended)

JAVA
Integer a = 1000, b = 1000;
if (a.equals(b)) {  // compares values, always correct
    System.out.println("Equal");
}

Method 2: Unbox to primitives

JAVA
Integer a = 1000, b = 1000;
if (a.intValue() == b.intValue()) {  // compares primitive values
    System.out.println("Equal");
}

Method 3: Objects.equals() for null safety

JAVA
import java.util.Objects;

Integer a = 1000;
Integer b = null;

// a.equals(b) works here, but b.equals(a) would throw NPE
System.out.println(Objects.equals(a, b));  // false, no NPE even if either is null

Objects.equals() handles null on both sides without throwing. Use it when either value might be null.

How does this affect HashMap?

HashMap uses .equals() and .hashCode() internally to find keys, not ==. So Integers as HashMap keys work correctly regardless of the cache.

JAVA
Map<Integer, String> accounts = new HashMap<>();
accounts.put(1000, "Rajesh");

Integer key = 1000;  // new Integer (outside cache)
System.out.println(accounts.get(key));  // "Rajesh" (HashMap uses equals + hashCode)

The lookup works because Integer.equals() compares values and Integer.hashCode() is consistent. If HashMap used ==, keys outside the cache would never match.

How is this similar to String interning?

Strings have a similar concept called the string intern pool. String literals are automatically interned (shared), so "hello" == "hello" is true. But new String("hello") == new String("hello") is false because each new creates a separate object.

JAVA
String s1 = "hello";              // interned (pool)
String s2 = "hello";              // same interned object
System.out.println(s1 == s2);     // true (same reference from pool)

String s3 = new String("hello");  // new object (heap)
String s4 = new String("hello");  // another new object
System.out.println(s3 == s4);     // false (different objects)
System.out.println(s3.equals(s4)); // true (same value)

The lesson is the same for both: == compares references and only accidentally works when objects are shared. .equals() compares values and always works.

What about switch on Integer?

Switch unboxes the Integer to int before matching, so it works correctly and is not affected by the cache:

JAVA
Integer branchCode = 1000;
switch (branchCode) {   // unboxes to int
    case 1000:
        System.out.println("Branch 1000");  // matches correctly
        break;
    default:
        System.out.println("Other");
}

But be careful: if branchCode is null, the unboxing throws NullPointerException.

Can you tune the cache to make == work for larger values?

Yes, but you should not rely on it.

BASH
# Raise the cache to cover values up to 10000
java -XX:AutoBoxCacheMax=10000 MyApp
# or
java -Djava.lang.Integer.IntegerCache.high=10000 MyApp

Now Integer.valueOf(1000) would return a cached object, and 1000 == 1000 would be true. But this is not portable: different deployments may have different settings. Never write code that depends on the cache boundary.

How do you detect this bug in code reviews?

Static analysis tools flag this automatically:

  • SpotBugs rule RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN flags == on boxed types.
  • SonarQube rule java:S5838 ("Equals should be used for Integer comparison").
  • IntelliJ IDEA inspection "Number objects compared using ==" shows a warning in the editor.

Configure these tools in your CI pipeline so this bug is caught before code review.

What are common mistakes?

  • Using == to compare Integer values. Works for small values in tests, fails for large values in production.
  • Raising the cache to fix the bug. This hides the bug instead of fixing it. The correct fix is .equals().
  • Forgetting null safety. a.equals(b) throws NPE if a is null. Use Objects.equals(a, b) when either side might be null.
  • Assuming all wrappers have caches. Float and Double are never cached. == on them always fails.

What should a senior engineer know?

  • The cache is allocated eagerly at class load time. Raising it to a very large value increases memory usage at startup. Each cached Integer is ~16 bytes on a 64-bit JVM, so a cache of 10,000 uses ~160 KB.
  • Byte is always fully cached. The entire byte range (-128 to 127) fits in the default cache, so == on Byte wrappers always works. But still use .equals() for consistency.
  • The JLS mandates caching for -128 to 127. This is not a HotSpot implementation detail. Any conforming JVM must cache at least this range. Extending beyond 127 is implementation-specific.
  • In Kotlin, == compares values (like Java's equals). Kotlin uses === for reference comparison. This eliminates the Integer cache trap entirely at the language level.

Visual Overview

DIAGRAM
  Integer a = 100           Integer a = 1000
       │                         │
       ▼                         ▼
  Integer.valueOf(100)      Integer.valueOf(1000)
       │                         │
  in cache? YES             in cache? NO
       │                         │
       ▼                         ▼
  return cache[228]         return new Integer(1000)
       │                         │
  Integer b = 100           Integer b = 1000
       │                         │
       ▼                         ▼
  Integer.valueOf(100)      Integer.valueOf(1000)
       │                         │
  return cache[228]         return new Integer(1000)
  (SAME object)             (DIFFERENT object)
       │                         │
       ▼                         ▼
  a == b → TRUE             a == b → FALSE
  (same reference)          (different references)
       │                         │
       ▼                         ▼
  a.equals(b) → TRUE        a.equals(b) → TRUE
  (value comparison)         (value comparison)


  Cache boundary:
    -128  ........................  127  |  128 ..... 1000 ..... MAX
    ◀─────── cached (== works) ─────────▶  ◀── not cached (== fails) ──▶


  Wrapper cache summary:
    Integer:   -128 to 127 (tunable upper)     Float:     never
    Long:      -128 to 127 (fixed)             Double:    never
    Short:     -128 to 127 (fixed)             Boolean:   always (2 values)
    Byte:      -128 to 127 (full range)        Character: 0 to 127

Key Takeaway

The reason Integer 100 compared with == returns true but Integer 1000 returns false is the Integer cache inside valueOf(). When you write Integer a = 100, the compiler calls Integer.valueOf(100), which returns a pre-created shared object from a cache that covers -128 to 127. Both a and b receive the same object, so == (which compares references) returns true.

For 1000, which is outside the cache, valueOf creates a new Integer object each time. The two references point to different objects, so == returns false even though the values are identical. This is not a bug in Java. It is the expected behavior of reference comparison on objects. The bug is in the code that uses == instead of .equals().

The fix is simple. Always compare wrapper objects with .equals(), which compares the underlying int values. Or unbox to primitives and compare with ==, which compares values directly. If either side might be null, use Objects.equals() for null safety.

The same trap exists for Long, Short, and Character (all cache -128 to 127). Byte is always cached because its entire range fits. Float and Double are never cached, so == on them always compares references.

One sentence to remember: == on Integer compares object identity and only accidentally works inside the -128 to 127 cache; always use .equals() to compare values.

---

Share
Use ← → arrow keys to navigate