Image of Working with hashcode() and equals() in java

ADVERTISEMENT

Table of Contents

Introduction

By default, the java super class java.lang.Object provides 2 important methods: equals() and hashcode() for comparing objects, these methods become very useful when implementing large business which requires interactions between several classes. In this article we talk about the relation between these methods, their default implementation and the circumstances which force developers to provide a custom implementation for each of them.

1. Method definition & default implementation

equals(Object obj): it is a method provided by java.lang.Object which indicates whether some other object passed as argument is “equal to” the current instance. The default implementation provided by the jdk is based on the memory location, so that 2 objects are equal if and only if they are stored in the same memory address.

hashcode(): it is a method provided by java.lang.Object which returns an integer representation of the object memory address. By default, this method returns a random integer which is unique for each instance, this integer might change between several executions of the application and wouldn’t stay the same.

2. Contract between equals() and hashcode()

The default implementation is not enough to satisfy business needs, especially if we’re talking about a huge application which considers 2 objects as equal when some business fact happens. In some business scenarios, developers provide their own implementation in order to force their own equality mechanism regardless the memory addresses.

As per java documentation, developers should override both methods in order to achieve a fully working equality mechanism and it’s not enough to just implement the equals() method.

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.

In the following sections, we provide several examples which show the importance of overriding both methods and the drawbacks of overriding equals() without hashcode().

3. Practical example

We define a class called Student as the following:

package com.programmer.gate.beans;
 
public class Student {
    
    private int id;
    private String name;
 
    public Student(int id, String name) {
        this.name = name;
        this.id = id;
    }
    
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}

For testing purposes, we define a main class HashcodeEquals which checks whether 2 instances of Student (who has the exact same attributes) are considered as equal.

public class HashcodeEquals {
 
    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");
        
        System.out.println("alex1 hashcode = " + alex1.hashCode());
        System.out.println("alex2 hashcode = " + alex2.hashCode());
        System.out.println("Checking equality between alex1 and alex2 = " + alex1.equals(alex2));
    }
}

Outout:

alex1 hashcode = 1852704110
alex2 hashcode = 2032578917
Checking equality between alex1 and alex2 = false

4. Overriding equals()

For business purposes, we consider that 2 students are equal if they have the same ID, so we override equals() method and provide our own implementation as the following:

@Override
    public boolean equals(Object obj) {
        if(obj==null) return false;
        if (!(obj instanceof Student))
            return false;
        if (obj == this)
            return true;
        return this.getId() == ((Student)obj).getId();
    }

In the above implementation, we are saying that 2 students are equal if and only if they are stored in the same memory address OR they have the same ID. Now if we try to run HashcodeEquals we will get the following output:

alex1 hashcode = 2032578917
alex2 hashcode = 1531485190
Checking equality between alex1 and alex2 = true

As you noticed, overriding equals() with our custom business forces java to consider the ID attribute when comparing 2 Student objects.

equals() with ArrayList

A very popular usage of equals() is defining an array list of Student and searching for a particular student inside it. So we modified our testing class in order the achieve this.

public class HashcodeEquals {
 
    public static void main(String[] args) {
        Student alex = new Student(1, "Alex");
        
        List<Student> studentsLst = new ArrayList<Student>();
        studentsLst.add(alex);
        
        System.out.println("Arraylist size = " + studentsLst.size());
        System.out.println("Arraylist contains Alex = " + studentsLst.contains(new Student(1,"Alex")));
    }
}

After running the above test, we get the following output:

Arraylist size = 1
Arraylist contains Alex = true

When calling studentsLst.contains(new Student(1,”Alex”)) , the list compares the passed object with all of its elements using the equals() method and since we already have a student with (id = 1) then it will return true, in case we didn’t override equals() the list will consider the passed argument as a totally new object and will return false.

5. Overriding hashcode()

Okay so we override equals() and we get the expected behavior even though the hash code of the 2 objects are different, so what’s the purpose of overriding hashcode()?

equals() with HashSet

Let’s consider a new test scenario, we want to store all the students in a HashSet, so we update HashcodeEquals as the following:

public class HashcodeEquals {
 
    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");
        
        HashSet<Student> students = new HashSet<Student>();
        students.add(alex1);
        students.add(alex2);
        
        System.out.println("HashSet size = " + students.size());
        System.out.println("HashSet contains Alex = " + students.contains(new Student(1,"Alex")));
    }
}

If we run the above test, we get the following output:

HashSet size = 2
HashSet contains Alex = false

WAIT !! we already override equals() and verified that alex1 and alex2 are equal, and we all know that HashSet stores unique objects, so why did it consider them as different objects ?

HashSet stores its elements in memory buckets, each bucket is linked to a particular hash code. When calling students.add(alex1), java stores alex1 inside a bucket and links it to the value of alex1.hashcode(). Now any time an element with the same hash code is inserted into the set, it will just replace alex1. However, since alex2 has a different hash code then it will be stored in a separate bucket and will be considered a totally different object.

Now when HashSet search for an element inside it, it first generates the element’s hash code and looks for a bucket which corresponds to this hash code.

Here comes the importance of overriding hashcode(), so let’s override it in Student and set it to be equal to the ID, so that students who have the same ID are stored in the same bucket:

@Override
    public int hashCode() {
        return id;
    }

Now if we try to run the same test, we get the following output:

HashSet size = 1
HashSet contains Alex = true

See the magic of hashcode() !! the 2 elements are now considered as equal and stored in the same memory bucket, so any time you call contains() and pass a student object holding the same hash code, the set will be able to find the element.

The same is applied for HashMap, HashTable and any data structure which uses hashing mechanism for storing elements.

6. Conclusion

In order to achieve a fully working custom equality mechanism, it is mandatory to override hashcode() each time you override equals(). Follow the below tips and you’ll never have leaks in your custom equality mechanism:

  • If 2 objects are equal, they MUST have the same hash code.
  • If 2 objects have the same hash code, it doesn’t mean that they are equal.
  • Overriding equals() alone will make your business fail with hashing data structures like: HashSet, HashMap, HashTable … etc.
  • Overriding hashcode() alone doesn’t force java to ignore memory addresses when comparing 2 objects.

Summary

By default, the java super class java.lang.Object provides 2 important methods: equals() and hashcode() for comparing objects, these methods become very useful when implementing large business which requires interactions between several classes. In this article we talk about the relation between these methods, their default implementation and the circumstances which force developers to provide a custom implementation for each of them.

Next Steps

If you're interested in learning more about the basics of Java, coding, and software development, check out our Coding Essentials Guidebook for Developers, where we cover the essential languages, concepts, and tools that you'll need to become a professional developer.

Thanks and happy coding! We hope you enjoyed this article. If you have any questions or comments, feel free to reach out to jacob@initialcommit.io.

Final Notes