By the end of this post, you will understand the contract between hashCode and equals methods in Java, comprehend
how hash-based collections use these methods for storage and retrieval, and recognize the consequences of breaking the
hashCode-equals contract in real applications.
Hash-based collections like HashMap and HashSet rely on two methods from the Object class: equals()
and hashCode(). These methods work together to determine object equality and efficient storage location. When Java
stores an object in aHashMap, it uses the hashCode to determine which bucket to place it in, then uses equals to
handle collisions within that bucket. Breaking the relationship between these methods causes objects to disappear from
collections, duplicate entries to appear, or lookups to fail mysteriously. This post explores how these methods
interact, why they must be overridden together, and what happens when their contract is violated.
Java defines a strict contract between hashCode and equals that all classes must follow. The Object class provides
default implementations, but custom classes often need to override them for meaningful equality comparisons.
The contract establishes three fundamental rules:
package academy.javapro;
public class ContractExample {
    public static void main(String[] args) {
        // Default Object behavior
        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Alice", 25);
        Person p3 = p1;
        // With default Object methods
        System.out.println("Default equals (p1 == p2): " + p1.equals(p2)); // false
        System.out.println("Default equals (p1 == p3): " + p1.equals(p3)); // true
        System.out.println("p1 hashCode: " + p1.hashCode());
        System.out.println("p2 hashCode: " + p2.hashCode());
        System.out.println("p3 hashCode: " + p3.hashCode());
        // With properly overridden methods
        Student s1 = new Student("Bob", 30);
        Student s2 = new Student("Bob", 30);
        System.out.println("\nOverridden equals: " + s1.equals(s2)); // true
        System.out.println("s1 hashCode: " + s1.hashCode());
        System.out.println("s2 hashCode: " + s2.hashCode());
        System.out.println("Equal objects have same hash: " +
                (s1.hashCode() == s2.hashCode())); // true
    }
    static class Person {
        String name;
        int age;
        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        // Uses default Object.equals() and Object.hashCode()
    }
    static class Student {
        String name;
        int age;
        Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Student student = (Student) obj;
            return age == student.age && name.equals(student.name);
        }
        @Override
        public int hashCode() {
            int result = name.hashCode();
            result = 31 * result + age;
            return result;
        }
    }
}The Person class uses default Object implementations. The equals method performs reference equality—two Person
objects are equal only if they're the same instance. Even though p1 and p2 have identical field values, they're
different objects in memory, so equals returns false. Their hash codes are also different because Object.hashCode()
typically returns values based on memory address.
The Student class overrides both methods properly. The equals method compares field values rather than references. Two
Student objects with the same name and age are considered equal. The hashCode method generates the same value for
equal objects, satisfying the contract requirement. The calculation uses the common pattern of multiplying by prime
number 31 and combining field hash codes.
The contract specifies these requirements:
- If two objects are equal according to equals(), they must have the same hash code
- If two objects have the same hash code, they are not required to be equal
- The hash code must remain consistent during an object's lifetime if the fields used in equals don't change
HashMap and HashSet use hashCode to determine storage location and equals to handle collisions. Understanding this
process reveals why both methods must work together correctly.
package academy.javapro;
import java.util.*;
public class HashMapMechanics {
    public static void main(String[] args) {
        demonstrateHashMapStorage();
        demonstrateHashSetBehavior();
    }
    static void demonstrateHashMapStorage() {
        Map<Point, String> locations = new HashMap<>();
        Point p1 = new Point(3, 4);
        Point p2 = new Point(3, 4);
        Point p3 = new Point(5, 6);
        // HashMap uses hashCode to find bucket
        locations.put(p1, "First location");
        locations.put(p3, "Second location");
        // Uses hashCode to find bucket, then equals to find exact key
        System.out.println("Get with same object: " + locations.get(p1));
        System.out.println("Get with equal object: " + locations.get(p2));
        System.out.println("Contains equal key: " + locations.containsKey(p2));
        // Different hash code means different bucket
        System.out.println("Map size: " + locations.size());
    }
    static void demonstrateHashSetBehavior() {
        Set<Point> points = new HashSet<>();
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);
        points.add(p1);
        boolean added = points.add(p2); // Won't add - equal to p1
        System.out.println("\nSet refused duplicate: " + !added);
        System.out.println("Set size: " + points.size());
        System.out.println("Contains p2: " + points.contains(p2));
    }
    static class Point {
        int x, y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (!(obj instanceof Point)) return false;
            Point point = (Point) obj;
            return x == point.x && y == point.y;
        }
        @Override
        public int hashCode() {
            return 31 * x + y;
        }
    }
}When HashMap stores p1, it calculates p1.hashCode() to determine the internal bucket. The value "First location"
goes into that bucket associated with key p1. When retrieving with p2, HashMap calculates p2.hashCode(), which
equals p1.hashCode() because the points are equal. HashMap looks in the same bucket and uses equals to confirm p2
matches p1, successfully returning "First location".
HashSet uses the same mechanism internally. When adding p2, HashSet calculates its hash code, finds the bucket
containing p1, then uses equals to detect they're the same. Since sets don't allow duplicates, the add operation
returns false and the set size remains 1.
Violating the hashCode-equals contract causes collections to malfunction. Objects become unretrievable, duplicates appear where they shouldn't, and collection operations produce incorrect results.
package academy.javapro;
import java.util.*;
public class BrokenContractExample {
    public static void main(String[] args) {
        demonstrateMissingHashCode();
        demonstrateMismatchedImplementations();
    }
    static void demonstrateMissingHashCode() {
        System.out.println("Missing hashCode override:");
        Map<BrokenKey, String> map = new HashMap<>();
        BrokenKey key1 = new BrokenKey(100);
        BrokenKey key2 = new BrokenKey(100);
        map.put(key1, "Value");
        System.out.println("key1 equals key2: " + key1.equals(key2));
        System.out.println("key1 hashCode: " + key1.hashCode());
        System.out.println("key2 hashCode: " + key2.hashCode());
        System.out.println("Retrieve with key1: " + map.get(key1));
        System.out.println("Retrieve with key2: " + map.get(key2)); // Returns null!
    }
    static void demonstrateMismatchedImplementations() {
        System.out.println("\nMismatched implementations:");
        Set<InconsistentKey> set = new HashSet<>();
        InconsistentKey k1 = new InconsistentKey(1, 2);
        InconsistentKey k2 = new InconsistentKey(1, 2);
        set.add(k1);
        set.add(k2); // Adds duplicate!
        System.out.println("Set size should be 1 but is: " + set.size());
        System.out.println("Contains k1: " + set.contains(k1));
        System.out.println("Contains k2: " + set.contains(k2));
    }
    // BROKEN - overrides equals but not hashCode
    static class BrokenKey {
        int id;
        BrokenKey(int id) {
            this.id = id;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (!(obj instanceof BrokenKey)) return false;
            return id == ((BrokenKey) obj).id;
        }
        // Missing hashCode override - uses Object.hashCode()
    }
    // BROKEN - hashCode doesn't match equals
    static class InconsistentKey {
        int x, y;
        InconsistentKey(int x, int y) {
            this.x = x;
            this.y = y;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (!(obj instanceof InconsistentKey)) return false;
            InconsistentKey key = (InconsistentKey) obj;
            return x == key.x && y == key.y; // Compares both fields
        }
        @Override
        public int hashCode() {
            return x; // Only uses x, ignores y!
        }
    }
}The BrokenKey class overrides equals but not hashCode. Even though key1.equals(key2) returns true, they have
different hash codes because they use Object's default hashCode implementation. HashMap places them in different
buckets. When searching with key2, HashMap looks in the wrong bucket and never finds the value, returning null despite
the keys being equal.
The InconsistentKey class has mismatched implementations. The equals method compares both x and y fields,
but hashCodeonly uses x. Two objects with the same x but different y would have the same hash code but not be equal,
violating the contract. In the example, even though the objects are equal, they might hash to different buckets
if Object.hashCode()interference occurs, causing duplicates in the HashSet.
Creating proper implementations requires including the same fields in both methods and following established patterns.
package academy.javapro;
import java.util.*;
public class CorrectImplementation {
    public static void main(String[] args) {
        // Test correct implementation
        Employee e1 = new Employee("EMP001", "Alice", 30);
        Employee e2 = new Employee("EMP001", "Alice", 30);
        Employee e3 = new Employee("EMP002", "Bob", 25);
        Map<Employee, String> directory = new HashMap<>();
        directory.put(e1, "Engineering");
        directory.put(e3, "Marketing");
        System.out.println("e1 equals e2: " + e1.equals(e2));
        System.out.println("e1 hashCode == e2 hashCode: " +
                (e1.hashCode() == e2.hashCode()));
        System.out.println("Retrieve with different instance: " +
                directory.get(e2));
        Set<Employee> employees = new HashSet<>();
        employees.add(e1);
        employees.add(e2); // Won't add duplicate
        employees.add(e3);
        System.out.println("Set size: " + employees.size()); // 2, not 3
    }
    static class Employee {
        private final String id;
        private final String name;
        private final int age;
        Employee(String id, String name, int age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            Employee employee = (Employee) obj;
            return age == employee.age &&
                    Objects.equals(id, employee.id) &&
                    Objects.equals(name, employee.name);
        }
        @Override
        public int hashCode() {
            return Objects.hash(id, name, age);
        }
    }
}The Employee class demonstrates correct implementation. The equals method checks all significant fields: id, name, and
age. It handles null values using Objects.equals() which safely compares objects that might be null. The hashCode
method uses Objects.hash() to combine the same fields, ensuring equal objects produce identical hash codes.
This implementation enables HashMap to correctly retrieve values using equal keys, even when using different object
instances. HashSet properly rejects duplicates based on logical equality rather than reference equality. The Objects
utility class, introduced in Java 7, simplifies implementation and reduces errors.
Best practices for implementation:
- Use the same fields in both equalsandhashCode
- Include all fields that determine logical equality
- Use Objects.hash()for simple hashCode implementation
- Make fields finalwhen possible to ensure consistent hash codes
- Consider using IDE generation tools or libraries like Lombok
The hashCode and equals methods form an inseparable contract that enables hash-based collections to function
correctly. When two objects are equal according to equals(), they must return the same hashCode(). HashMap and
HashSet rely on this contract—hashCode determines the storage bucket while equals handles collisions and confirms
equality. Breaking this contract by overriding only one method or implementing them inconsistently causes objects to
vanish from collections, allows duplicates in sets, and makes map retrievals fail mysteriously. Correct implementation
requires using the same fields in both methods, with Objects.hash() and Objects.equals() simplifying the process.
Understanding this relationship prevents subtle bugs and ensures collections behave predictably when storing custom
objects.