The Relationship Between equals() and hashCode() Methods and Their Impact on HashMap
This article explains how Java's default equals() compares object references, why overriding both equals() and hashCode() is required for correct behavior in collections like HashMap, and demonstrates the proper implementations with code examples and a detailed look at HashMap's internal mechanics.
In Java, every class inherits from Object, which provides the default equals(Object obj) method that compares object addresses and a native hashCode() method that returns an int derived from the object's address.
When a class does not override equals, equality checks fall back to reference comparison, which is often unsuitable for business logic. Overriding equals without also overriding hashCode violates the contract that equal objects must have equal hash codes, leading to unexpected behavior in hash‑based collections.
Example: a Stock class with price and name fields is created. Using the default equals, two distinct Stock instances with identical field values are considered unequal, so a.equals(b) returns false.
public class Test {
public static class Stock {
public int price;
public String name = "";
public Stock(int price, String name) {
this.price = price;
this.name = name;
}
}
public static void main(String[] ss) {
Stock a = new Stock(10, "QNR");
Stock b = new Stock(10, "QNR");
System.out.println(a.equals(b)); // false
}
}After overriding equals to compare price and name, a.equals(b) returns true. However, if hashCode is not overridden, inserting both objects as keys into a HashMap creates two separate entries because their hash codes differ.
public static class Stock {
public int price;
public String name = "";
public Stock(int price, String name) {
this.price = price;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj instanceof Stock) {
Stock s = (Stock) obj;
return this.price == s.price && this.name.equals(s.name);
}
return false;
}
}Running the same main method now prints true, but the HashMap still treats the two keys as distinct because their hash codes are inconsistent.
public static void main(String[] ss) {
Stock a = new Stock(10, "QNR");
Stock b = new Stock(10, "QNR");
Map<Stock, String> map = new HashMap<>();
map.put(a, "qunar");
map.put(b, "qunar");
for (Entry<Stock, String> en : map.entrySet()) {
Stock stock = en.getKey();
System.out.println(stock.name + "_" + stock.price + "==>" + en.getValue());
}
}The output shows two identical lines, confirming that the map contains two separate entries. Overriding hashCode to produce the same value for equal objects resolves the issue:
@Override
public int hashCode() {
return new StringBuilder().append(price).append(name).toString().hashCode();
}Key points:
Both equals and hashCode must be overridden together.
Equal objects must return identical hash codes.
The article then explains the internal structure of HashMap. A HashMap maintains an array called table, where each slot holds a linked list of Entry objects. When keys collide, they are stored in the same bucket as a chain.
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next; // reference to next entry in the bucket
final int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}The put method computes the hash of the key, determines the bucket index with indexFor(hash, table.length), traverses the bucket's chain to find an existing key, and either replaces the value or adds a new Entry. If the map exceeds its load factor, it resizes the table and rehashes all entries.
public V put(K key, V value) {
if (key == null) return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}The indexFor method uses a bitwise AND because the table length is always a power of two, making the operation equivalent to a modulo but much faster.
static int indexFor(int h, int length) {
return h & (length - 1);
}The get method mirrors the lookup logic of put, computing the hash, locating the bucket, and traversing the chain to find a matching key.
public V get(Object key) {
if (key == null) return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}In summary, overriding both equals and hashCode consistently ensures that equal objects behave correctly as keys in hash‑based collections, preventing duplicate entries and enabling reliable retrieval.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Qunar Tech Salon
Qunar Tech Salon is a learning and exchange platform for Qunar engineers and industry peers. We share cutting-edge technology trends and topics, providing a free platform for mid-to-senior technical professionals to exchange and learn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
