Back to Blog
Java

Java HashMap equals and hashCode

java hashmap equals hashcode: Understand how equals and hashCode affect HashMap lookups, why the contract matters, and how to implement keys correctly.

HashMapequalshashCodeJava collectionsobject equality
Illustration of Java HashMap buckets with keys and hashed values.

When you store objects in a HashMap, Java uses two methods to locate values: hashCode() to find the bucket and equals() to identify the exact key within that bucket. Many developers first encounter java hashmap equals hashcode when a lookup returns null even though the key appears to be present. The cause is usually an inconsistent implementation of these two methods. This article explains how the two methods interact, why the contract between them is critical, and how to implement them correctly for reliable map behavior.

The Bucket Mechanism

A HashMap does not compare a new key directly against every stored key. It first calls hashCode() on the key and uses the result to select a bucket (an index in the internal array). If the bucket is empty, the key is not present. If the bucket contains entries, the map then calls equals() to compare the new key with each stored key in that bucket. This two-step process is why hashCode() must be consistent with equals(). If two objects are equal, they must produce the same hash code. If they do not, they may end up in different buckets, so equals() is never called and the lookup fails.

The default hashCode() implementation in Object returns a value derived from the object's memory address. The default equals() also uses reference equality. For most value objects that you create, you need to override both methods to define logical equality.

The Contract Between equals and hashCode

The Java Language Specification defines the contract as follows:

  • If a.equals(b) is true, then a.hashCode() must equal b.hashCode().
  • If a.equals(b) is false, hashCode() may still return the same value for both (a collision).
  • Calling hashCode() multiple times on the same object during a single run must return the same value, provided the object hasn't changed.

The first rule is the one most often violated. Consider a Person class with a name field, where equals() compares names but hashCode() is not overridden. Two Person instances with the same name will be equal, but their default hash codes will almost certainly differ. Storing one in a HashMap and then looking it up with the other will fail because the lookup key lands in a different bucket.

The second rule is important because it allows unequal objects to share a hash code. For example, two different strings can have a colliding hash. That is acceptable because the map will call equals() to distinguish them. The cost is that a bucket with multiple entries requires a linear scan.

Implementing equals and hashCode

Suppose you have a simple Book class with an ISBN and a title. For a book, the ISBN is a natural unique identifier.

public class Book { private final String isbn; private final String title; public Book(String isbn, String title) { this.isbn = isbn; this.title = title; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Book)) return false; Book other = (Book) o; return isbn.equals(other.isbn); } @Override public int hashCode() { return isbn.hashCode(); } }

Here, equals() checks that the ISBNs match, and hashCode() is derived from the ISBN only. This respects the contract: any two books with the same ISBN are equal and produce the same hash code. If two books have the same title but different ISBNs, they are unequal, and their hash codes may be the same if the titles coincide—that's legal, and the map handles it correctly.

Notice that equals() must handle the null check and the type check. Using instanceof allows comparison with subclasses, which may or may not be desirable. If you have strict equality requirements, use getClass() instead. Your choice depends on the class hierarchy.

Impact of Hash Collisions

Hash collisions occur when unequal keys produce the same hashCode(). When that happens, multiple entries land in the same bucket, and HashMap stores them in a linked list (or a tree in certain versions when the bucket becomes large). Operations then degrade from O(1) average to O(n) in the worst case. A poor hashCode() that returns a constant for all objects forces every lookup to scan every entry. That behavior can turn an efficient map into a linear search.

A well-distributed hashCode() reduces collisions, but you should not over-optimize. The default String.hashCode() is reasonable for most cases. For your own classes, combine the hash codes of the fields used in equals(). For example:

@Override public int hashCode() { int result = 1; result = 31 * result + isbn.hashCode(); result = 31 * result + title.hashCode(); return result; }

Using the same fields in both methods ensures that any change to equality semantics is reflected in the hash calculation. The multiplier 31 is a common choice because it produces a good spread and is easy to compute.

Mutable Keys and Consistency

The contract requires that hashCode() returns the same value for the lifetime of the object used as a key. If you mutate a field that participates in hashCode(), the stored hash becomes stale. A HashMap uses the hash at the time of insertion to locate the bucket. If the hash changes later, the map will look for the key in a different bucket, and the entry becomes effectively orphaned.

Map<Person, String> map = new HashMap<>(); Person alice = new Person("Alice"); map.put(alice, "Engineer"); alice.setName("Bob"); // hash changes if name is in hashCode() String result = map.get(new Person("Alice")); // may return null

To avoid this problem, make key classes immutable or ensure that the fields used in hashCode() and equals() are never modified after the object is placed in the map. If you must mutate such fields, use a mutable key only with a map that supports identity semantics, or use a different data structure. For production code, prefer immutable keys such as String, Integer, or custom immutable value types.

Common Mistakes and Debugging

A frequent mistake is overriding equals() without hashCode(), or vice versa. The compiler does not warn you about this because both are declared in Object. When a lookup fails, the map logic silently returns null. To diagnose, check the following:

  • Are the two objects logically equal according to equals()?
  • Do they return the same hashCode()?
  • Has the value of any field used in equals() or hashCode() changed after insertion?

A quick test is to call key1.equals(key2) and key1.hashCode() == key2.hashCode() on the object used for insertion and the lookup key. If equality is true but hash codes differ, the map search goes to the wrong bucket. If hash codes match but equality is false, the entry may be in the right bucket but a different logical key.

Performance Considerations in Production

When an object becomes a key in a high-traffic HashMap, the cost of hashCode() and equals() matters. An expensive hashCode() slows every insertion and lookup. An expensive equals() only matters when collisions exist, but if a bad hash causes many collisions, the map may spend significant time comparing keys. For production systems, keep hashCode() fast by using only a few fields and arithmetic operations, and ensure equals() short-circuits cheaply (such as comparing a single ID field).

Another consideration is memory and allocation. Storing complex objects as keys means the map holds strong references to them. If you use mutable keys and change them, you risk both hash inconsistency and accidental retention of data that you intended to discard. Prefer immutable value objects for keys, which also makes the code easier to reason about.

Advanced Case: Records Can Help

If you are using Java 16 or later, records provide a simple way to create value objects with correct equals() and hashCode() implementations. The compiler generates both methods based on all accessor fields.

public record BookRecord(String isbn, String title) {}

BookRecord instances compare by ISBN and title, and their hash is derived from both. If you need equality based on only part of the record, you can override the generated methods, but doing so requires care. For most cases, using a record as a key is the most reliable approach because it removes the risk of writing an inconsistent pair by hand.

When an existing class is not a record and cannot be changed, you can wrap it in a new immutable key type that computes equality and hash based on the desired fields. That keeps the map stable without modifying the original class.

The interaction between equals and hashCode is the backbone of HashMap, and getting it wrong leads to obscure bugs that are hard to trace. By respecting the contract, keeping keys immutable, and choosing a hash that reflects equality, you ensure that the map behaves predictably under load and over time.

java hashmap equals hashcode: Practical Usage and Code Examp | RYUSLOG DEV