If you’ve ever seen this surprising behavior in Java:
Integer a = 1;
Integer b = 1;
System.out.println(a == b); // true
Integer x = 1000;
Integer y = 1000;
System.out.println(x == y); // false
— you’re not alone: this trips up many Java developers. Let’s unpack exactly why this happens, what the rules are, what’s guaranteed by the language, how to avoid bugs, and examples you can run.
(short summary)
- == compares primitive values but object references for reference types.
- Integer is an object wrapper for the primitive int. When you use autoboxing (e.g., Integer i = 1;), Java may reuse cached Integer objects for certain values.
- Java caches Integer objects for -128 to 127 by default. So Integer a = 1; Integer b = 1; will point to the same cached object → == returns true.
- 1000 is outside the cache range, so two autoboxed Integer objects are different objects → == returns false.
- Always use .equals() (or compare primitive int values) to compare numeric values reliably.
Important concepts to understand first
1. Primitive vs Reference types
- int is a primitive. int a = 1; int b = 1; → a == b compares values (true).
- Integer is an object (wrapper class). Integer a = new Integer(1); Integer b = new Integer(1); → a == b compares references (different objects → false) while a.equals(b) compares values (true).
2. Autoboxing / Unboxing
- Autoboxing: automatic conversion from primitive to wrapper (e.g., int → Integer) when needed.
- Integer i = 1; is autoboxing int 1 into an Integer.
- Unboxing: wrapper → primitive when needed (e.g., Integer i; int j = i;).
3. == vs .equals()
- == on primitives: compares values.
- == on object references: compares whether both references point to the same object.
- .equals() (for wrapper classes) compares values (unless overridden differently).
4. Integer caching (
Integer.valueOf
)
- Integer.valueOf(int i) may return cached Integer objects for a range of values.
- The Java SE spec requires caching for at least -128 to 127. Many JVMs allow configuring a higher positive bound (via -Djava.lang.Integer.IntegerCache.high=…) but you should not rely on anything beyond the spec default.
- Autoboxing uses Integer.valueOf(…) internally.
Concrete example — run this and inspect output
public class IntegerEqualityDemo {
public static void main(String[] args) {
// Primitives: always compares values
int p1 = 1;
int p2 = 1;
System.out.println("int 1 == int 1 : " + (p1 == p2)); // true
// Autoboxed Integers in cache range (-128..127)
Integer a = 1; // autoboxing -> Integer.valueOf(1)
Integer b = 1; // same cached object
System.out.println("Integer 1 == Integer 1 : " + (a == b)); // true
System.out.println("Integer 1 equals Integer 1 : " + a.equals(b)); // true
// Autoboxed Integers outside cache range
Integer x = 1000; // autoboxing -> Integer.valueOf(1000) typically creates new object
Integer y = 1000; // another object
System.out.println("Integer 1000 == Integer 1000 : " + (x == y)); // usually false
System.out.println("Integer 1000 equals Integer 1000 : " + x.equals(y)); // true
// Using new -> always different objects
Integer n1 = new Integer(1);
Integer n2 = new Integer(1);
System.out.println("new Integer(1) == new Integer(1) : " + (n1 == n2)); // false
System.out.println("new Integer(1) equals new Integer(1) : " + n1.equals(n2)); // true
// Forcing unboxing: compares primitives
System.out.println("x.intValue() == y.intValue() : " + (x.intValue() == y.intValue())); // true
System.out.println("x == 1000 : " + (x == 1000)); // true -> x is unboxed here to primitive int
// Identity hash codes to show different objects
System.out.println("System.identityHashCode(a): " + System.identityHashCode(a));
System.out.println("System.identityHashCode(b): " + System.identityHashCode(b));
System.out.println("System.identityHashCode(x): " + System.identityHashCode(x));
System.out.println("System.identityHashCode(y): " + System.identityHashCode(y));
}
}
Expected output (typical on standard JVM):
int 1 == int 1 : true
Integer 1 == Integer 1 : true
Integer 1 equals Integer 1 : true
Integer 1000 == Integer 1000 : false
Integer 1000 equals Integer 1000 : true
new Integer(1) == new Integer(1) : false
new Integer(1) equals new Integer(1) : true
x.intValue() == y.intValue() : true
x == 1000 : true
System.identityHashCode(a): 460141958
System.identityHashCode(b): 460141958
System.identityHashCode(x): 1163157884
System.identityHashCode(y): 1956725890
System.identityHashCode values show that a and b are the same object (same id), while x and y are different objects (different ids).
Why
Integer a = 1; Integer b = 1; is true
- Integer a = 1; is autoboxing and uses Integer.valueOf(1).
- For 1 (in -128..127), Integer.valueOf returns a cached shared Integer object.
- a and b reference the same object, so a == b is true.
Why Integer x = 1000; Integer y = 1000; is false
- 1000 is outside the mandatory cache range.
- Integer.valueOf(1000) will typically create separate Integer objects (or at least distinct references).
- So x and y do not point to the same object; x == y → false.
- Their .equals() compares numeric values and returns true.
Some more edge cases and useful checks
1. Compiler constant folding for Integer references
If values are compile-time constants and boxed at compile-time, you may sometimes see surprising behavior. But the safe rule is: do not rely on == for Integer comparison.
2. short, byte, char, wrappers also have caching behavior
Short, Byte, Character (for small ranges), Long (often cached -128..127) follow similar caching patterns in valueOf.
3. Configuring cache range
Some JVM implementations allow configuring the upper bound of Integer cache with a system property:
-Djava.lang.Integer.IntegerCache.high=1000
But this is implementation-specific and should not be relied upon in application logic.
4. equals() vs ==
best practice
Always use .equals() when comparing wrapper numbers for equality of numeric value:
Integer a = 1000;
Integer b = 1000;
if (a.equals(b)) {
// correct way to check numeric equality
}
Or unbox to primitives to compare:
if (a.intValue() == b.intValue()) { ... }
or
if (a == b.intValue()) { ... } // a unboxed or b unboxed
Performance note
Some people worry about .equals() vs == performance. The overhead of .equals() on wrapper types is negligible for typical application logic and runnable Java code will inline and optimize such calls; correctness is far more important.
Common bugs caused by incorrectly using ==
- Comparing values in collections or caches where wrappers are used as keys.
- Conditional checks: if (myInteger == 0) { … } — this may unbox myInteger and is usually OK, but if myInteger is null you’ll get NullPointerException. Safer: Integer.valueOf(0).equals(myInteger) or Objects.equals(myInteger, 0).
- HashMap/Set behaviours depending on equals vs identity.
Handy checklist (short)
- Want to compare numeric value? Use .equals() (or unbox to primitives).
- Want to check if two references point to the same object? Use ==.
- Don’t rely on == for wrapper classes — it may produce different results because of caching and object identity.
- Beware of null when unboxing wrappers — safest is to null-check first.
| Code | Result (==) | Result (.equals()) | Explanation |
|---|---|---|---|
| int a=1; int b=1; | true | n/a | primitive compare |
| Integer a=1; b=1; | true | true | cached Integer objects |
| Integer a=1000; b=1000; | false | true | outside cache -> distinct objects |
| Integer a=new Integer(1); | false | true | new always creates new object |
| Integer a=null; a==0 | NPE on unbox | n/a | unboxing null throws NullPointerException |
Closing notes
The 1 == 1 vs 1000 == 1000 paradox is not a bug in Java — it’s a side-effect of autoboxing, caching, and the difference between reference comparison and value comparison. Knowing the difference and following the simple rule — use .equals() for wrapper value equality — will prevent most real-world problems.
If you like, I can:
- Provide a downloadable Java file you can run locally.
- Show how to detect identity vs value equality in debugging sessions.
- Create a short unit test demonstrating these cases.