o7planning

Understanding Java System.identityHashCode, Object.hashCode and Object.equals

  1. The equals() contract
  2. System.identityHashCode(Object)
  3. Object.hashcode()
  4. HashCode() & equals() consistency violation

1. The equals() contract

equals(Object) method is used to compare the current object with another object based on the values of the properties of each object. You can override this method in your class.
public boolean equals(Object other)
Example: Money class with 2 properties: currencyCode & amount. Two Money objects are considered equal by equals() method if they have the same currencyCode and amount:
Money.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class Money {
    private String currencyCode;
    private int amount;

    public Money(String currencyCode, int amount) {
        this.amount = amount;
        this.currencyCode = currencyCode;
    }

    public int getAmount() {
        return amount;
    }

    public String getCurrencyCode() {
        return currencyCode;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Money)) {
            return false;
        }
        Money o = (Money) other;
        return this.amount == o.amount //
                && Objects.equals(this.currencyCode, o.currencyCode);
    }
}
When overriding equals() method, you need to comply with the following criteria, they are called an equals() contract:
1
Reflexive
An object must equal itself
2
Symmetric
x.equals(y) must return the same result as y.equals(x).
3
Transitive
if x.equals(y) and y.equals(z) then also x.equals(z).
4
Consistent
The value of x.equals(y) does not change if the properties involved in the comparison do not change. (Randomness is not allowed).
Symmetric
The symmetry of equals() needs to be guaranteed, in other words if x.equals(y) then y.equals(x). This sounds simple, but sometimes you violate it unintentionally.
For example, the WrongVoucher class below violates the symmetry of the equals() contract:
WrongVoucher.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class WrongVoucher extends Money {
    private String store;

    public WrongVoucher(String store, String currencyCode, int amount) {
        super(currencyCode, amount);
        this.store = store;
    }

    public String getStore() {
        return store;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof WrongVoucher)) {
            return false;
        }
        WrongVoucher o = (WrongVoucher) other;
        
        return this.getAmount() == o.getAmount() //
                && Objects.equals(this.getCurrencyCode(), o.getCurrencyCode()) //
                && Objects.equals(this.store, o.store);
    }
}
At first glance, WrongVoucher class and its equals() method appear to be correct. It works perfectly if you compare 2 WrongVoucher objects, but you will see problems if you compare WrongVoucher object with Money object and vice versa.
WrongVoucherTest.java
package org.o7planning.equals.ex;

public class WrongVoucherTest {
    
    public static void main(String[] args)  {
        Money m = new Money("USD", 100);
        WrongVoucher wv = new WrongVoucher("Chicago S1", "USD", 100);
        
        System.out.println("m.equals(wv): " + m.equals(wv)); // true
        System.out.println("wv.equals(m): " + wv.equals(m)); // false
    }
}
Output:
m.equals(wv): true
wv.equals(m): false
To avoid the above trap, we can rewrite Voucher class and use Money as a property instead of inheriting from Money.
Voucher.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class Voucher {
    private String store;
    private Money money;

    public Voucher(String store, String currencyCode, int amount) {
        this.store = store;
        this.money = new Money(currencyCode, amount);
    }

    public String getStore() {
        return store;
    }

    public Money getMoney() {
        return money;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Voucher)) {
            return false;
        }
        Voucher o = (Voucher) other;
        
        return Objects.equals(this.store, o.store) //
                && this.money.equals(o.money);
    }
}

2. System.identityHashCode(Object)

In Java, System.identityHashCode(obj) static method returns the identity hashcode of the obj object, which is a non-negative integer between [0, 2^31-1]. The identity hashcode of a null object is 0.
@HotSpotIntrinsicCandidate
public static native int identityHashCode(Object x);
According to the design idea, identity hashcode of different objects should be different. However this is not guaranteed absolutely. JVM's algorithm can only guarantee that the probability of duplicate identity hashcode is very small. Identity hashcode of an object is only calculated at the first moment when it is actually used and stored in the object's Header.
Identity hashcode is certainly not generated based on the address of the object in memory. Unfortunately, there is no documentation about the identity hashcode generation algorithm, the secret lies in the source code of the JVM written in C++ language. I will update on this algorithm if more information becomes available.

3. Object.hashcode()

hashCode() method of java.lang.Object class returns hashcode of the current object, which is exactly the identity hashcode of that object.
public class Object  {
    
    public int hashCode() {
        return System.identityHashCode(this);
    }
}
Example of hashcode and identity hashcode of a pure object (new java.lang.Object()).
HashCodeEx1.java
package org.o7planning.hashcode.ex;

public class HashCodeEx1 {

    public static void main(String[] args) {
        Object obj1 = new Object();
        
        int idHashcode = System.identityHashCode(obj1);
        int hashcode = obj1.hashCode();
        
        System.out.println("Identity Hashcode: " + idHashcode);
        System.out.println("Hashcode: " + hashcode);
    }
}
Output:
Identity Hashcode: 1651191114
Hashcode: 1651191114
Descendant classes of java.lang.Object can override hashCode() method to return a custom value but need to ensure the following rules, which is also called hashCode() contract .
1
Equals consistency
If two objects are equal according to equals(Object) method, their hashCode() method must return the same value.
2
Internal consistency
The value of hashCode() can change only if the properties participating in equals(Object) method change.
Two objects that are not equal according to equals(Object) method do not necessarily have different hashcode(s). However, two different objects with different hashcode values will improve the performance of Hash table (See more explanation in the article about HashMap and HashSet).
See more:
  • Java HashSet
HashCodeEx2.java
package org.o7planning.hashcode.ex;

public class HashCodeEx2 {

    public static void main(String[] args) {
        Employee tom = new Employee("Tom");
        Employee jerry = new Employee("Jerry");
        
        System.out.println("Employee: " + tom.getFullName());
        System.out.println("  - Identity hashcode: " + System.identityHashCode(tom));
        System.out.println("  - Hashcode: " + tom.hashCode());
        
        System.out.println("\nEmployee: " + jerry.getFullName());
        System.out.println("  - Identity hashcode: " + System.identityHashCode(jerry));
        System.out.println("  - Hashcode: " + jerry.hashCode());
    }
}

class Employee {
    private String fullName;

    public Employee(String fullName) {
        this.fullName = fullName;
    }
    
    public String getFullName()  {
        return this.fullName;    
    }

    @Override
    public int hashCode() {
        if (this.fullName == null || this.fullName.isEmpty()) {
            return 0;
        }
        char ch = this.fullName.charAt(0);
        return (int) ch;
    }
}
Output:
Employee: Tom
  - Identity hashcode: 1579572132
  - Hashcode: 84

Employee: Jerry
  - Identity hashcode: 359023572
  - Hashcode: 74

4. HashCode() & equals() consistency violation

Basically, when your class overrides equals(Object) method, you also have to override hashCode() method to make sure that 2 objects that are equal by equals(Object) method will have the same hashcode. This is necessary and safe when you use the object of this class as a key of *HashMap (HashMap, WeakHashMap, IdentityHashMap,...).
The BadTeam class below violates Equals consistency:
BadTeam.java
package org.o7planning.equals.ex;

import java.util.Objects;

public class BadTeam {
    private String name;
    private int numberOfMembers;
    
    public BadTeam(String name, int numberOfMembers) {
        this.name = name;
        this.numberOfMembers = numberOfMembers;
    }
    public String getName() {
        return name;
    }
    public int getNumberOfMembers() {
        return numberOfMembers;
    }
    
    @Override
    public boolean equals(Object other)  {
        if(this == other)  {
            return true;
        }
        if(!(other instanceof BadTeam))  {
            return false;
        }
        BadTeam o = (BadTeam) other;
        return Objects.equals(this.name, o.name);
    }
    
    @Override
    public int hashCode()  {
        return this.numberOfMembers;
    }
}
BadTeamTest.java
package org.o7planning.equals.ex;

public class BadTeamTest {

    public static void main(String[] args) {
        BadTeam team1 = new BadTeam("Team 1", 3);
        BadTeam team2 = new BadTeam("Team 1", 5);
        
        boolean isEquals = team1.equals(team2); // true
        
        int hashcode1 = team1.hashCode(); // 3
        int hashcode2 = team2.hashCode(); // 5
        
        System.out.println("team1.equals(team2): " + isEquals); // true
        System.out.println("hashcode1 == hashcode2: " + (hashcode1 == hashcode2)); // false
    }
}
Output:
team1.equals(team2): true
hashcode1 == hashcode2: false
Violation of hashCode() contract may result when you use *HashMap class (HashMap, WeakHashMap, IdentityHashMap,..). Things may not work as you expect.
HashMap_BadTeam_Test.java
package org.o7planning.equals.ex;

import java.util.HashMap;

public class HashMap_BadTeam_Test {

    public static void main(String[] args) {

        // BadTeam team --> String leader.
        HashMap<BadTeam, String> map = new HashMap<>();

        BadTeam team1 = new BadTeam("Team 1", 3);
        BadTeam team2 = new BadTeam("Team 1", 5);
        
        map.put(team1, "Tom");
        map.put(team2, "Jerry");
        
        BadTeam team = new BadTeam("Team 1", 10);
        
        String leader = map.get(team);
        System.out.println("Leader of " + team.getName() + " is " + leader);
    }
}
Output:
Leader of Team 1 is null
See also how HashMap, WeakHashMap and IdentityHashMap store data to understand more what was mentioned above:

Java Basic

Show More