Java equals() and hashcode()

Introduction

In Java, the equals() and hashCode() methods are used to define object equality and to enable the effective use of objects in hash-based data structures such as HashMapHashSet, etc. These methods are part of the Object class, which is the root class for all Java objects. However, they are often overridden in custom classes to provide meaningful equality comparisons and hash code generation.

Understanding the equals() and hashCode() contract in Java is crucial for implementing correct and efficient solutions. Neglecting their proper implementation can lead to significant issues in the applications we develop.

How does equals() work?

The default implementation of the equals() method in the Object class compares objects by their reference. This means that two objects are considered equal if they are the same object in memory. However, this is not always the right solution.

Syntax:

				
					public boolean equals(Object obj) {
    return (this == obj);
}
				
			

In many cases, this default behavior is not what we want, especially when dealing with custom classes. To correctly compare the content of objects, we need to override the equals() method in our class. Java SE specifies the requirements that our implementation of the equals() method should meet. These criteria are straightforward and aim to ensure consistency.

The equals() method must adhere to the following principles:

  1. Reflexive: An object must always be equal to itself.
  2. Symmetric: When comparing two objects, x.equals(y) must yield the same result as y.equals(x).
  3. Transitive: If object x equals object y and object y equals object z, then object x must also equal object z.
  4. Consistent: The result of equals() should remain constant unless a property that is considered within equals() changes. It should not produce random results.

Example: We have created a BankAccount class with three attributes.

				
					import java.util.Objects;

public class BankAccount {
    private String accountNumber;
    private String accountHolderName;
    private double balance;

    public BankAccount(String accountNumber, String accountHolderName, double balance) {
        this.accountNumber = accountNumber;
        this.accountHolderName = accountHolderName;
        this.balance = balance;
    }

    // Getters and setters

    public static void main(String [] args) {
        BankAccount account1 = new BankAccount("Savings", "Test", 2022);
        BankAccount account2 = new BankAccount("Savings", "Test", 2022);
        System.out.println(account1.equals(account2)); // returns false
    }
}
				
			

In this example, we have created two objects of BankAccount class in the main method. When we compare both objects using the equals() method it returns a false even after all the three attribute values are same.

Now let’s override the equals method in BankAccount class:

				
					import java.util.Objects;

public class BankAccount {

    //Fields And Constructors

    // Getters and setters


   // Equals method implementation
   @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        BankAccount otherAccount = (BankAccount) obj;
        return Objects.equals(accountNumber, otherAccount.accountNumber)
            && Objects.equals(accountHolderName, otherAccount.accountHolderName)
            && Double.compare(balance, otherAccount.balance) == 0;
    }

     public static void main(String [] args) {
        BankAccount account1 = new BankAccount("Savings", "Test", 2022);
        BankAccount account2 = new BankAccount("Savings", "Test", 2022);
        System.out.println(account1.equals(account2)); // returns true
    }
}
				
			

In this example, the equals() method checks if two BankAccount objects are equal by comparing their individual attributes using Objects.equals() for string comparison and Double.compare() for comparing the balance. Now the equals method returns true when we compare account1 and account2.

How does hashCode() work?

The hashCode() method is used to generate a numeric hash code for an object. This hash code is used by hash-based data structures like HashMap to quickly locate and manage objects. When we store objects in hash-based collections, the objects are stored based on their hash codes. If two objects have the same hash code, the data structure will use the equals() method to check for actual equality.

To ensure the proper functioning of hash-based collections, it is crucial to override the hashCode() method when we override the equals() method.

hashCode() Contract

Java SE defines a clear contract for the hashCode() method, and upon careful examination, it becomes evident that hashCode() and equals() are closely interrelated. The hashCode() contract includes three critical criteria, all of which mention the equals() method:

  1. Internal Consistency: The value returned by hashCode() should only change if a property that is used in the equals() method changes. In other words, if two objects are equal according to equals(), they must produce the same hashCode().
  2. Equals Consistency: Objects that are equal to each other, as determined by the equals() method must consistently return the same hashCode() value.
  3. Collisions: It is possible for unequal objects to have the same hashCode(). Since the number of possible hash codes is limited (due to the finite number of integers), it’s natural for different objects to produce the same hash code. In such cases, the equals() method should be used to distinguish between the objects and resolve any potential collisions.

Example: Let’s print the hashCode of two objects of BankAccount class as defined in the above example before implementing the hashCode() method:

				
					    public static void main(String [] args) {
        BankAccount account1 = new BankAccount("Savings", "Test", 2022);
        BankAccount account2 = new BankAccount("Savings", "Test", 2022);
        System.out.println(account1.equals(account2)); // true
        System.out.println(account1.hashCode());       // 168423058
        System.out.println(account2.hashCode());       // 821270929

    }
				
			

In the above example, we can see hashCode of both the equals are different even though both objects are equal. Let’s override the hashCode() method for BankAccount class:

				
					import java.util.Objects;

public class BankAccount {

    //Fields And Constructors

    // Getters and setters   
    @Override
    public int hashCode() {
        int result;
        long temp;
        result = accountNumber.hashCode();
        result = 31 * result + accountHolderName.hashCode();
        temp = Double.doubleToLongBits(balance);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

     public static void main(String [] args) {
        BankAccount account1 = new BankAccount("Savings", "Test", 2022);
        BankAccount account2 = new BankAccount("Savings", "Test", 2022);
        System.out.println(account1.equals(account2)); // true
        System.out.println(account1.hashCode());       // -813975577
        System.out.println(account2.hashCode());       // -813975577

    }
}
				
			

This example shows that after overriding the hashCode() method, both objects have the same hashCode when they are equal.

Conclusion

The hashCode() and equals() contract is a fundamental concept in Java, ensuring the proper functioning of applications when dealing with object comparison and hashing. This contract defines a set of rules that must be followed to maintain consistency and correctness. In essence, the contract specifies that the hashCode() method should return the same hash code value for objects that are considered equal based on the equals() method. Additionally, it emphasizes that if two objects are equal, their hash codes must also be equal.

HashCode serves as a shortcut for optimizing object comparison since it provides a numeric representation of an object’s state. On the other hand, the equals() method is responsible for precisely checking the equality of objects, usually by comparing their content or key properties. By adhering to this contract, developers can ensure that objects behave as expected during operations like searching, adding, or removing elements from collections, making the application efficient and reliable. It prevents unexpected behavior and data inconsistencies that might otherwise arise.