ZetCode

Java equals and hashCode

Last modified: May 07, 2025

The equals and hashCode methods play a fundamental role in Java's object comparison and hashing mechanisms. A well-implemented equals method ensures accurate object equality checks, while an optimized hashCode method enables efficient use of objects in hash-based collections such as HashMap and HashSet. Properly overriding these methods is essential for maintaining consistent and predictable behavior in Java applications.

These methods are originally defined in the Object class, the root of Java's class hierarchy. Since all Java objects inherit these methods by default, understanding their behavior is crucial. By default, equals checks for reference equality (whether two object references point to the same memory address), and hashCode generates a unique identifier based on the object's memory location.

However, for custom classes, overriding these methods is often necessary to provide meaningful equality comparisons based on an object's state (such as attribute values) rather than reference identity. When overriding equals, ensure consistency with hashCode—objects that are considered equal according to equals must return the same hash code. This alignment prevents unexpected behavior in hash-based collections and ensures efficient data retrieval.

The equals Method

The equals method is used to compare two objects for equality. The default implementation in the Object class provides reference equality (using the == operator). Most classes should override this to provide value-based equality, comparing the actual content of objects.

Let's define a Person class that we will use in our examples. It includes fields for name, age, and passport number, along with correctly implemented equals and hashCode methods.

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {

    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getPassportNumber() {
        return passportNumber;
    }

    @Override
    public boolean equals(Object o) {
        // 1. Check if the same object instance
        if (this == o) return true;
        
        // 2. Check if null or different class
        if (o == null || getClass() != o.getClass()) return false;
        
        // 3. Cast to the correct type and compare significant fields
        Person person = (Person) o;
        return age == person.age &&
               Objects.equals(name, person.name) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, passportNumber);
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

The following EqualsDemo class demonstrates the usage of the equals method from our Person class.

EqualsDemo.java
package com.zetcode;

public class EqualsDemo {

    public static void main(String[] args) {

        // Assumes Person.java is compiled and in the classpath,
        // or in the same project/package.
        Person p1 = new Person("John Doe", 30, "A123456");
        Person p2 = new Person("John Doe", 30, "A123456"); // Same data as p1
        Person p3 = new Person("Jane Smith", 25, "B654321"); // Different data
        
        System.out.println("p1: " + p1);
        System.out.println("p2: " + p2);
        System.out.println("p3: " + p3);

        System.out.println("p1 equals p2: " + p1.equals(p2)); // true
        System.out.println("p1 equals p3: " + p1.equals(p3)); // false
        System.out.println("p1 equals null: " + p1.equals(null)); // false

        // Comparing with an object of a different type
        String s = "A string object";
        System.out.println("p1 equals String object: " + p1.equals(s)); // false
    }
}

This example demonstrates a proper equals implementation within the Person class. The EqualsDemo shows how it correctly compares Person objects based on their content (value equality). The method checks for self-comparison, null, and class equality before comparing fields. The Objects.equals helper handles null-safe field comparisons.

The equals Contract

The equals method must adhere to a specific contract defined in the Java documentation to ensure predictable behavior:

The Person.java class used in the following test is the same as defined in the previous section.

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {

    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassportNumber() { return passportNumber; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
               Objects.equals(name, person.name) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, passportNumber);
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

The following ContractTest class demonstrates these contract properties using instances of our Person class.

ContractTest.java
package com.zetcode;

public class ContractTest {

    public static void main(String[] args) {

        Person a = new Person("Alice", 25, "P123");
        Person b = new Person("Alice", 25, "P123"); // Equal to a
        Person c = new Person("Alice", 25, "P123"); // Equal to a and b
        
        // Reflexive: a.equals(a)
        System.out.println("Reflexive (a.equals(a)): " + a.equals(a)); // true
        
        // Symmetric: a.equals(b) should be same as b.equals(a)
        System.out.println("Symmetric (a.equals(b)): " + a.equals(b));    // true
        System.out.println("Symmetric (b.equals(a)): " + b.equals(a));    // true
        
        // Transitive: if a.equals(b) and b.equals(c), then a.equals(c)
        boolean abEquals = a.equals(b);
        boolean bcEquals = b.equals(c);
        if (abEquals && bcEquals) {
            System.out.println("Transitive (a.equals(c)): " + a.equals(c)); // true
        }
        
        // Consistent: multiple calls to a.equals(b) return same result
        System.out.println("Consistent 1 (a.equals(b)): " + a.equals(b)); // true
        System.out.println("Consistent 2 (a.equals(b)): " + a.equals(b)); // true
        
        // Non-null: a.equals(null) should be false
        System.out.println("Non-null (a.equals(null)): " + a.equals(null)); // false
    }
}

This ContractTest example helps verify that our Person class's equals method satisfies all parts of the contract. Violating any of these rules can lead to unpredictable and incorrect behavior, especially when objects are used in Java Collections Framework classes.

The hashCode Method

The hashCode method returns an integer hash code value for an object. This value is primarily used by hash-based collections like HashMap, HashSet, and Hashtable to efficiently store and retrieve objects. The general contract for hashCode is critical: if two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

If you override equals, you must override hashCode as well. Our Person class (shown again below for clarity) already includes a correct hashCode implementation.

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {
    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassportNumber() { return passportNumber; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
               Objects.equals(name, person.name) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

    @Override
    public int hashCode() {
        // Use Objects.hash() to combine hash codes of all fields
        // used in the equals() method.
        return Objects.hash(name, age, passportNumber);
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

The HashCodeDemo class demonstrates the behavior of this hashCode.

HashCodeDemo.java
package com.zetcode;

public class HashCodeDemo {

    public static void main(String[] args) {

        Person p1 = new Person("John Doe", 30, "A123456");
        Person p2 = new Person("John Doe", 30, "A123456"); // Equal to p1
        Person p3 = new Person("Jane Smith", 25, "B654321"); // Not equal to p1
        
        System.out.println("p1: " + p1 + ", hashCode: " + p1.hashCode());
        System.out.println("p2: " + p2 + ", hashCode: " + p2.hashCode());
        System.out.println("p3: " + p3 + ", hashCode: " + p3.hashCode());
        
        // Crucial check: if p1.equals(p2) is true, then
        // p1.hashCode() must be equal to p2.hashCode().
        boolean equalsContractMet = p1.equals(p2) && (p1.hashCode() == p2.hashCode());
        System.out.println("Equal objects (p1, p2) have same hash code: " + equalsContractMet); // true

        // Note: If p1.equals(p3) is false, their hash codes are not
        // required to be different, but it's good if they are for performance.
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // false
        System.out.println("Are hashCodes of p1 and p3 different? " +
                           (p1.hashCode() != p3.hashCode())); // usually true
    }
}

This example shows a proper hashCode implementation in the Person class using Objects.hash. This utility method computes a hash code based on the hash codes of all provided fields. It's important that all fields used in the equals method are also used in the hashCode calculation. Equal objects (p1 and p2) produce the same hash code. Unequal objects (p1 and p3) should ideally produce different hash codes to ensure good performance in hash tables.

The hashCode Contract

The hashCode method must adhere to this contract:

The Person.java class used in the test below is the same one we have been using, with correct equals and hashCode methods.

Person.java
package com.zetcode;

import java.util.Objects;

public class Person {

    private String name;
    private int age;
    private String passportNumber;

    public Person(String name, int age, String passportNumber) {
        this.name = name;
        this.age = age;
        this.passportNumber = passportNumber;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getPassportNumber() { return passportNumber; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
               Objects.equals(name, person.name) &&
               Objects.equals(passportNumber, person.passportNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, passportNumber);
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + 
               ", passportNumber='" + passportNumber + "'}";
    }
}

The HashCodeContractTest verifies these rules.

HashCodeContractTest.java
package com.zetcode;

public class HashCodeContractTest {

    public static void main(String[] args) {

        Person a = new Person("Alice", 25, "P123");
        Person b = new Person("Alice", 25, "P123"); // Equal to a
        Person c = new Person("Bob", 30, "P456");   // Different from a
        
        // Consistency: a.hashCode() called multiple times returns same value
        int hash1 = a.hashCode();
        int hash2 = a.hashCode();
        System.out.println("Consistent hash codes (hash1 == hash2): " + (hash1 == hash2)); // true
        
        // Equality correlation: a.equals(b) implies a.hashCode() == b.hashCode()
        boolean equalsIsTrue = a.equals(b);
        boolean hashCodesEqual = (a.hashCode() == b.hashCode());
        System.out.println("Equal objects, equal hash (a.equals(b) && a.hashCode() == b.hashCode()): " +
                           (equalsIsTrue && hashCodesEqual)); // true
                           
        // Inequality implication: !a.equals(c)
        // a.hashCode() != c.hashCode() is desirable but not guaranteed.
        boolean notEqual = !a.equals(c);
        boolean hashCodesDifferent = (a.hashCode() != c.hashCode());
        System.out.println("Unequal objects (a and c), are their hash codes different? " +
                           hashCodesDifferent); // usually true for good hash functions
        if (notEqual && !hashCodesDifferent) {
            System.out.println("Note: Unequal objects a and c have a hash collision.");
        }
    }
}

This HashCodeContractTest example verifies the hashCode contract using our Person class. While unequal objects can have the same hash code (a "hash collision"), good hashCode implementations aim to minimize collisions to maintain efficient hash table performance.

Best Practices for equals and hashCode

When implementing these methods, consider the following best practices:

Employee.java
package com.zetcode;

import java.util.Objects;
import java.util.HashMap;
import java.util.Map;

public class Employee {
    private final int id; // Immutable, unique identifier
    private String name;
    private String department;
    
    public Employee(int id, String name, String department) {
        if (name == null || department == null) {
            throw new NullPointerException("Name and department cannot be null");
        }
        this.id = id; // 'id' is final, set only once
        this.name = name;
        this.department = department;
    }
    
    public int getId() { return id; }
    public String getName() { return name; }
    public void setName(String name) {
        if (name == null) throw new NullPointerException("Name cannot be null");
        this.name = name;
    }
    public String getDepartment() { return department; }
    public void setDepartment(String department) {
        if (department == null) throw new NullPointerException("Department cannot be null");
        this.department = department;
    }

    @Override
    public final boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Employee)) return false; 
        Employee employee = (Employee) o;
        return id == employee.id;
    }
    
    @Override
    public final int hashCode() {
        return Objects.hash(id);
    }
    
    @Override
    public String toString() {
        return "Employee{id=" + id + ", name='" + name + "', department='" + department + "'}";
    }

    public static void main(String[] args) {

        Employee e1 = new Employee(101, "John Smith", "Engineering");
        Employee e2 = new Employee(101, "Johnathan Smith", "HR"); 
        Employee e3 = new Employee(102, "Alice Wonderland", "Engineering");
        
        System.out.println("e1 equals e2 (same ID): " + e1.equals(e2)); // true
        System.out.println("e1 hashCode == e2 hashCode: " + (e1.hashCode() == e2.hashCode())); // true

        Map<Employee, String> employeeMap = new HashMap<>();
        employeeMap.put(e1, "Access Card A");
        e1.setDepartment("Management"); // Change mutable field
        // Still retrievable as equals/hashCode depend only on 'id'
        System.out.println("Value for e1 after department change: " + employeeMap.get(e1)); // Access Card A
        System.out.println("Value for e2 (equal to e1): " + employeeMap.get(e2)); // Access Card A
    }
}

This Employee example demonstrates a class where equality is based solely on an immutable unique identifier (id). The mutable fields (name and department) do not participate in the equals or hashCode calculations. This ensures that an Employee object's hash code remains constant and its equality is stable, making it safe for hash-based collections even if mutable attributes change.

Common Pitfalls

Several common mistakes can occur when implementing equals and hashCode, leading to subtle bugs:

PitfallsDemo.java
package com.zetcode;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class PitfallsDemo {

    static class BadSymmetryPoint {
        private final int x;
        private final int y;
        public BadSymmetryPoint(int x, int y) { this.x = x; this.y = y; }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o instanceof BadSymmetryPoint) {
                BadSymmetryPoint that = (BadSymmetryPoint) o;
                return x == that.x && y == that.y;
            }
            if (o instanceof String) { // Problematic comparison
                return o.equals(String.format("(%d,%d)", x, y));
            }
            return false;
        }
        @Override
        public int hashCode() { return Objects.hash(x,y); }
    }
    
    static class MutableKey {
        private int value;
        public MutableKey(int value) { this.value = value; }
        public void setValue(int value) { this.value = value; }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            return value == ((MutableKey) o).value;
        }
        @Override
        public int hashCode() { return Objects.hash(value); }
        @Override
        public String toString() { return "MutableKey{value=" + value + "}"; }
    }

    public static void main(String[] args) {

        BadSymmetryPoint p1 = new BadSymmetryPoint(1, 2);
        String s1 = "(1,2)";
        System.out.println("p1.equals(s1): " + p1.equals(s1)); // true
        System.out.println("s1.equals(p1): " + s1.equals(p1)); // false (Symmetry violated!)
        System.out.println("---");

        Set<MutableKey> keySet = new HashSet<>();
        MutableKey key = new MutableKey(42);
        keySet.add(key);
        System.out.println("Set contains key (value 42): " + keySet.contains(key)); // true
        key.setValue(99); // Modify key's state (and hashCode)
        System.out.println("Set contains modified key (value 99): " + keySet.contains(key)); // false!
    }
}

The PitfallsDemo class illustrates two common problems. First, BadSymmetryPoint violates symmetry in equals. Second, MutableKey shows why using mutable objects as keys in hash collections is dangerous: modifying the key after insertion can make it "lost".

Lombok and IDE Generation

Manually writing equals and hashCode can be error-prone. Tools like Project Lombok (with @EqualsAndHashCode) and IDE code generation features can create these methods automatically, reducing boilerplate and potential mistakes while ensuring contract adherence.

ToolAssistedExample.java
package com.zetcode;

import lombok.EqualsAndHashCode; 
import java.util.Objects;

@EqualsAndHashCode // Lombok: generates equals() and hashCode()
class LombokPerson {
    private String name;
    private int age;
    private String email;
    public LombokPerson(String name, int age, String email) {
        this.name = name; this.age = age; this.email = email;
    }
}

class IDEGeneratedPerson { // Example of IDE-generated methods
    private String name;
    private int age;
    private String email;
    public IDEGeneratedPerson(String n, int a, String e) { name=n; age=a; email=e; }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        IDEGeneratedPerson p = (IDEGeneratedPerson) o;
        return age == p.age && Objects.equals(name, p.name) && Objects.equals(email, p.email);
    }
    @Override
    public int hashCode() { return Objects.hash(name, age, email); }
}

public class ToolAssistedExample {
    public static void main(String[] args) {

        LombokPerson lp1 = new LombokPerson("Alice", 30, "a@e.com");
        LombokPerson lp2 = new LombokPerson("Alice", 30, "a@e.com");
        System.out.println("Lombok: lp1 equals lp2: " + lp1.equals(lp2)); // true
        
        IDEGeneratedPerson ip1 = new IDEGeneratedPerson("Carol", 35, "c@e.com");
        IDEGeneratedPerson ip2 = new IDEGeneratedPerson("Carol", 35, "c@e.com");
        System.out.println("IDE: ip1 equals ip2: " + ip1.equals(ip2)); // true
    }
}

This example showcases Lombok and typical IDE generation. These tools help maintain correctness, but understanding the underlying principles remains vital.

Simplified Implementation with Java Records

Since Java 14 (standardized in Java 16), records offer a concise way to create immutable data carriers. The compiler automatically generates equals, hashCode, toString, a canonical constructor, and accessor methods for all record components.

RecordExample.java
package com.zetcode;

import java.util.HashSet;
import java.util.Set;

record UserRecord(int id, String username, String email) {
    // Compiler auto-generates constructor, accessors,
    // equals(), hashCode(), and toString() based on all components.
}

public class RecordExample {
    public static void main(String[] args) {

        UserRecord user1 = new UserRecord(1, "john.doe", "john.doe@example.com");
        UserRecord user2 = new UserRecord(1, "john.doe", "john.doe@example.com");
        UserRecord user3 = new UserRecord(2, "jane.doe", "jane.doe@example.com");

        System.out.println("user1 equals user2: " + user1.equals(user2)); // true
        System.out.println("user1 hashCode == user2 hashCode: " + 
                           (user1.hashCode() == user2.hashCode())); // true

        Set<UserRecord> userSet = new HashSet<>();
        userSet.add(user1);
        System.out.println("Set contains user2 (equal to user1): " + 
                           userSet.contains(user2)); // true
        System.out.println("User1 details: " + user1.toString());
    }
}

The UserRecord class, being a Java record, automatically gets correct equals and hashCode. This significantly reduces boilerplate for data classes and ensures contract adherence.

Source

Java Language Specification: Object.equals()
Java Language Specification: Object.hashCode()
Java Language Features: Records

In this article, we've examined Java's equals and hashCode. We covered contracts, implementation, best practices, pitfalls, and modern approaches. Proper understanding is essential for correct object comparison and reliable behavior in Java collections.

Author

My name is Jan Bodnar, and I am a dedicated programmer with many years of experience in the field. I began writing programming articles in 2007 and have since authored over 1,400 articles and eight e-books. With more than eight years of teaching experience, I am committed to sharing my knowledge and helping others master programming concepts.

List all Java tutorials.