Back to Blog
Java

Java Object hashCode: Contract and Implementation

java object hashcode: Learn the Java hashCode contract, why overriding equals requires hashCode, and how to implement it correctly to avoid collection bugs.

hashCodeequalsJava collectionsobject contracthashing
Diagram showing the relationship between hashCode and equals in Java objects.

When you place a custom object into a HashMap or HashSet, the Java runtime calls that object's hashCode() method to determine its bucket. If you have ever seen a collection return unexpected results or fail to find an object you just inserted, the cause is often a broken hashCode implementation. The java object hashcode contract is simple, but it has consequences that affect correctness, performance, and maintainability.

The hashCode Contract

Every Java object inherits a hashCode() method from Object. The general contract, as defined in the Object class documentation, has three parts:

  1. Consistency: If you call hashCode() on the same object more than once during one execution of a Java application, it must return the same integer, provided no information used in equals comparisons has changed.
  2. Equal objects must have equal hashes: If two objects are equal according to equals(Object), then calling hashCode() on both must produce the same result.
  3. Unequal objects may have equal hashes: This is not a requirement, but it is allowed. Collisions are inevitable because hashCode() returns an int, while there are more possible object states than 2^32.

The second point is the one that most often causes bugs. If you override equals() to provide logical equality, you must also override hashCode() so that equal objects land in the same hash bucket. Failing to do so breaks the contract and makes collections like HashMap and HashSet behave incorrectly.

Why Overriding equals Requires Overriding hashCode

Consider a simple Person class with name and age fields. If you override equals() to compare those fields but leave hashCode() as the default identity-based implementation, two Person instances with the same name and age will be equal but will have different hash codes.

When you insert one instance into a HashSet and then try to check whether the set contains a logically equal instance, the set computes the hash of the second instance, finds a different bucket, and never calls equals(). The result is false, even though the objects are equal. The same problem occurs with HashMap keys and any hash-based collection.

The fix is to ensure that hashCode() uses the same fields that equals() uses. That way, equal objects always produce the same hash, and the collection can find the correct bucket before calling equals() to confirm equality.

A Minimal Correct Implementation

Here is a straightforward implementation for a class with two fields:

public class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } }

The Objects.hash method (introduced in Java 7) combines the hash codes of the provided fields using a fixed algorithm. It is concise and avoids manual arithmetic. The equals method uses Objects.equals to handle null safely for the name field.

This implementation satisfies the contract: equal objects produce the same hash, and the hash is consistent as long as the fields do not change. Because name and age are final, the object is immutable, which makes the hash stable for the object's lifetime.

Choosing Fields for the Hash

Which fields should you include in hashCode()? The rule is simple: use the same fields that you use in equals(). If you include a field in equals(), it must appear in hashCode(). If you exclude a field from equals(), you can exclude it from hashCode() as well.

In practice, you should include fields that are significant for logical equality. Avoid fields that are derived from others or that are not part of the object's identity. For example, a cached computed value or a transient flag should not be included.

Another consideration is mutability. If a field can change after the object is placed in a hash-based collection, the hash code will change, and the object will be lost in the collection. This is a common source of subtle bugs. Prefer immutable fields for hash computation. If you must use mutable fields, ensure that the object is not used as a key in a hash-based collection while its state changes.

Common Mistakes and Their Runtime Effects

Several mistakes appear frequently in real code:

  • Overriding equals without hashCode: This breaks the contract and causes hash-based collections to fail. The runtime does not throw an error; it simply returns incorrect results.
  • Using mutable fields in hashCode: If a field changes after the object is inserted into a HashMap, the hash changes, and the entry becomes unreachable. This can lead to memory leaks because the entry is never found again.
  • Including fields that are not in equals: This is not a contract violation, but it can cause performance issues because equal objects may have different hashes, forcing them into different buckets.
  • Using the default hashCode when logical equality is needed: The default hashCode() is based on the object's memory address, which is almost never what you want for value objects.
  • Returning a constant hash: Some developers simply return 42 to satisfy the contract. This is correct but disastrous for performance because every object lands in the same bucket, turning a hash table into a linked list.

Each of these mistakes has a different runtime effect, but they all degrade the behavior of hash-based collections. The first three cause correctness issues, while the last one causes severe performance degradation.

Performance Considerations for Hash Functions

The quality of a hash function directly affects the performance of HashMap, HashSet, and Hashtable. A good hash function distributes objects evenly across buckets, minimizing collisions. When collisions occur, the collection must resolve them, typically with a linked list or a tree in modern Java implementations. High collision rates increase lookup time from O(1) toward O(n).

The Objects.hash method is convenient, but it creates an array to hold the arguments, which adds small overhead. For most applications this is negligible. If you are writing a class that is used in very large collections or in performance-critical code, you might implement a manual hash calculation using a prime multiplier:

@Override public int hashCode() { int result = 17; result = 31 * result + name.hashCode(); result = 31 * result + age; return result; }

The choice of 31 is traditional because it is an odd prime and produces a good distribution for many inputs. The exact value is not critical; what matters is that you combine fields in a way that is sensitive to their order and values. The manual approach avoids the array allocation of Objects.hash and can be faster in tight loops.

Another performance consideration is caching the hash code. If the object is immutable, you can compute the hash once and store it in a field. This avoids recomputing the hash on every call, which is useful when the object is used frequently as a key. However, caching adds memory overhead and is only worthwhile if the hash computation is expensive and the object is used repeatedly.

When to Rely on the Default Implementation

The default hashCode() from Object is based on the object's identity, which is usually the memory address. This is appropriate when you do not need logical equality. For example, if you are using an object as a lock or in an identity-based collection such as IdentityHashMap, the default implementation is correct.

If your class does not override equals(), then two instances are equal only if they are the same reference. In that case, the default hashCode() is consistent with equals() because both are identity-based. Overriding hashCode() without overriding equals() would be unusual and often incorrect.

For value objects like String, Integer, and custom domain objects that represent data, you should always override both equals() and hashCode(). The default implementation is rarely what you want for such classes.

Testing hashCode Consistency

Because the contract is precise, you can write simple unit tests to verify that your implementation is correct. A basic test should check that two equal objects have the same hash code and that the hash is stable across calls.

@Test void equalObjectsHaveSameHashCode() { Person alice1 = new Person("Alice", 30); Person alice2 = new Person("Alice", 30); assertEquals(alice1, alice2); assertEquals(alice1.hashCode(), alice2.hashCode()); } @Test void hashIsStable() { Person alice = new Person("Alice", 30); int firstHash = alice.hashCode(); assertEquals(firstHash, alice.hashCode()); }

These tests do not guarantee a good distribution, but they catch the most common contract violations. For a more thorough test, you could generate many objects with varying fields and verify that the hash codes are not all identical, though that is more of a performance heuristic than a correctness requirement.

A practical way to test integration with collections is to insert objects into a HashSet and verify that contains works for equal instances. This exercises the full hash-and-equals flow and often reveals subtle bugs that unit tests on individual methods miss.

java object hashcode: Practical Usage and Code Examples | RYUSLOG DEV