Thursday, June 25, 2026

Always compare objects (including Integer and Long etc) with equals

 The difference between equals() and == in Java is known to everyone. There is a minute detail, though, which I will explain a moment later.

Consider the situation:

Map<String, Integer> map = new HashMap<>();
map.put("a",128);
map.put("b",128);

map.put("c",1100);
map.put("d",1100);

map.put("e",5);
map.put("f",5);

System.out.println(map.get("a") == map.get("b"));
System.out.println(map.get("c") == map.get("d"));
System.out.println(map.get("e") == map.get("f"));

The first two return false, while the third one returns true.
This means that if you depend on ==  for equality comparison of two wrapper objects, your code can silently fail.

Why do you need to compare?
Various reasons based on your requirement. One common reason could be using them in a Comparator. Whatever be the reason, using == can fail you and your code.

Why does it fails? 
JVM caches primitives to some extent. For example, int and long values -128 to 127 are cached. The range is decided by the writers of JLS (Java Language Specification) to improve performance by avoiding repeated creation and garbage collection of objects for common use cases such as loops, basic counts or array indexing. Similarly, the booleans true and false are cached too.

When you use java.lang.Integer, or do boxing such as in the map values above, new Object is created which resides on the heap. Comparing them means comparing object. If the value falls within the cached values, JVM object simply references or points to the cache object.
So when you have Integer a = 128; JVM creates a new Object on the heap. When you have Integer b = 120, JVM find the value in the cache and just points to that, preventing a new Object to be managed.
When you use primitives, int a = 128, this is just a member of the thread stack or sits inside the object of the class where its defined. 

Now see below:
Integer a = 130, b = 130;
System.out.println(a==b); // false
System.out.println(a==130); // true

First one gives false since objects are compared, and both are different by virtue of their hashcodes even if their content is same.
Second line gives true because upon seeing the ==, JVM just unboxes the "a" to primitive 130 and quickly compares.

An actual code:

Map<String, Integer> map = new HashMap<>();
map.put("a", 130);
map.put("b", 120);
map.put("c", 130);
map.put("e", 140);
map.put("f", 150);

        List<String> sortedList = new ArrayList<>();
sortedList.addAll(map.keySet());
Comparator<Integer> comparator = (str1, str2) -> {
// reverse sort on same values
if(map.get(str1) == map.get(str2)) {
return str2.compareTo(str1);
}
// normal sort on different values
return Integer.compare(map.get(str1), map.get(str2));
};
        Collections.sort(sortedList, comparator);

The expectation is that the list should be sorted based on the below conditions:
1. If count is the same for multiple strings, sort them in reverse order
2. If the counts of two strings are not same, sort them in a normal ascending or lexicographical order.

Our expected output should be : [b, c, a, e, f].
Notice how "a" and "c" are in reverse order.

But if you run the above code, you will get: [b, a, c, e, f].
"a" and "c" never were reverse sorted because the == actually compared two different objects with different hash code, ensuring our goal was not met. 
Were we having some other strings with the same count within the cache-able range, i.e. -128 to 127, this == would have worked flawlessly.
This means our code is now flaky with == comparison of two Objects. Only way it will work as expected is to use equals():

            if(map.get(str1).equals(map.get(str2))) {
return str2.compareTo(str1);
}
On paper and IDE, == looked safe until it silently failed.

Conclusion:
The most important thing to remember is not to depend on things that confuse or feel flaky, strictly use whats advised: == for primitives and .equals for objects (wrapper classes like Integer, Long are objects after all).