How to use equals() and hashCode() in Java

Justgiveacar
9 min readJul 6, 2021

Hi there !

Today I’m going to talk about one of questions that are important but easily ignore by us : equals() and hashCode() methods in Java.

I found both these methods extremely important in the development process, so if the person knows nothing about them, it can cause a lot of the issues in the application like missing data inside the hashed data structures, wrong equality process results, etc.

So, come with me and let’s move on to discover these 2 methods.

As you might know, there is a class Object, that is a parent of each other class in Java, which means that if we create a new class Humanity in such a way :

public class Humain { // TODO}

It will implicitly extend the Object class. That's why, even if it seems that our Humanity class is empty, it has several methods available inherited from the Object class:

  • equals()
  • hashCode()
  • toString()
  • getClass()
  • notify()
  • notifyAll()
  • wait()

As rest of the methods are out of the scope of the current article 😅, let’s focus on equals() and hashCode() ones.

equals()

So equals(Object o) method is used to indicate whether some other Object o is equal to the current object (on which the method has been invoked).

The explanation looks very clear, but one question could appear: “Wait, why do we need this method, we have == operation will be enough right ?"

Well, thanks for this common question, but I have to say == operator works perfectly for the primitive data types like int, long, boolean, etc. But for the non-primitive (or reference) data type (like Humanity, Animal, or even Alien) == operator shows not the equality of the objects, but whether they refer to the same object in memory. It works in such a way due to Java Memory Model (JMM) : primitives and reference objects are kept in different memory parts.

Let me demonstrate this in code.

public class Humanity {
private String name;
public Humanity(String name){
this.name = name;
}
}
class Main {
public static void main(String[] args) {
Humanity h1 = new Humanity("Linzhou");
Humanity h2 = new Humanity("Simpson");
System.out.println(h1 == h2);
}
}

We’ll have the following output:

false

But what would it be, if use equals() method to compare these 2 Humanity instances instead of == operator ?

class Main {
public static void main (String[] args) {
Humanity h1 = new Humanity("Linzhou");
Humanity h2 = new Humanity("Simpson");
System.out.println(h1.equals(h2));
}
}

Would it return true? Nope, the output is false again. How come ? The answer is simple: right now we are using the Object class implementation of equals() method,so let's take a look how it looks like:

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

As we see, it calls the == operator under the hood, so the false output looks more clear more.

Of course, this implementation doesn’t satisfy our needs, so we can override it.

But first let’s postpone the overriding process a little bit to learn several equals() method rules, that are going to help us in our implementation:

  1. Reflexive: a.equals(a) must always return true (of course, if a != null, otherwise NullPointerException will be thrown);
  2. Symmetric: if a.equals(b) return true, then b.equals(a) must always return true as well.
  3. Transitive: if a.equals(b) returns true and a.equals(c) returns true, it means, that b.equals(c) must be true as well.
  4. Consistent: if a.equals(b) return true (or false) and neither a nor b has been changed, a.equals(b) should always return the same result.

The rules are simple and intuitive, but anyway it is extremely important to follow them to omit the possbile issues in the apllication.

Let’s get back to our Humanity comparison problem:
I propose not to hurry up, but take a look at the method signature to think, how to override it correctly. As you might have noticed, equals() method accepts Object o as a parameter, so it will be impossible to write something like this:

@Override
public boolean equals(Object o){
return this.name.equals(o.name);
}

as Object class knows notoriously nothing about name property - compiler error. Also, this code could throw NullPointerException if name == null or o == null. And, yes, we could write something like this:

public boolean equals(Humanity h){
return name.equals(h.name);
}

But it will not the overriding, but the overloading.
There are a lot of libraries and APIs, that use equals() method under the hood, so as you might have understood, they know nothing about our overloading version of the equals() method, but will call that one from the Object class and our comparison issue will appear again. It means that the overloading is not an appropriate solution for our case.

As we have seen before, the Object class equals() implementation uses == operator to compare the objects. If two objects point to the same object in memory, they are definitely equal. It's a good point to start our overriding.

  1. Compare objects using == operator.
@Override
public boolean equals(Object o){
return this == o;
}

We have mentioned above, that we could face the NullPointerException if Object o is equal to null. Alse, we do know, that our current object (Humanity instance, on which we are going to call equals() method) is not null for sure, as otherwise the NullPointerException will be thrown on method call on null Humanity instance, but it is not our business right now. It goes with another step:

  1. Check, whether the Object o parameter is not null. If yes, return false.
@Override
public boolean equals(Object o){
if (this == o)
return true;
if (o == null)
return false;
return true;
}

Looks better, but not enough. As we have mentioned before, there is one more difficulty left: o is Object type, so it's impossible to reach Humanity data from it. Of course, we could cast it to Humanity and it seems that we are good to go. But, unfortunately, it is not safe. Just take a look at the code:

@Override
public boolean equals(Object o){
if (this == o)
return true;
if (o == null)
return false;
Humanity h = (Humanity) o;
return this.name.equals(h.name);
}

Let me introduce the new class Dog:

public class Dog{
private String name;
public Dog(String name)
this.name = name;
}

Take a look how to break our equals() implementation:

class Main{
public static void main (String[] args){
Humanity h = new Humanity("Linzhou");
Dog dog = new Dog("poppy");
System.out.println(h.equals(dog));
}
}

Unfortunately there is no compile error, as even dog is Dog type, but it is an Object type as well(Object is a parent of each class), so it is allowed to pass the dog inside our method. But if we run this code, we'll have an exception:

Exception in thread "main" java.lang.ClassCastException: Dog cannot be cast to Humanity

How can we cast a dog to a humanity, which makes no sense right ?

  1. Check, whether the Object o parameter has the same type, as our instance. If no, return false.
@Override
public boolean equals(Object o){
if (this == o)
return true;
if (o == null)
return false;
if (this.getClass() != o.getClass())
return false;
return true;
}

Here we have used getClass() method, that comes from the Object class. This method just returns the Class of the object, that calls it. So for dog.getClass(); it returns Dog, for h.getClass() which returns Humanity and so on.

  1. Cast the Object to the current class
  2. Compare the chosen fields values.
@Override
public boolean equals(Object o){
if (this == o)
return true;
if (o == null)
return false;
if (this.getClass() != o.getClass())
return false;
Humanity h = (Humanity) o;
return Objects.equals(this.name, h.name);
}

We have used Objects.equals() method here to simplify the logic and omit this.name null checking, as it has been already implemented inside the used method of Objects class:

public static boolean equals(Object a, object b){
return (a == b) || (a != null && a.equals(b));
}

Let’s try to check, whether it works correctly:

class Main {
public static void main(String[] args){
Humanity h1 = new Humanity("Linzhou");
Humanity h2 = new Humanity("Linzhou");
Dog dog = new Dog("poppy");
System.out.println(h1.equals(h2));
System.out.println(h1.equals(dog));
}
}

The output is finally predictable:
true and false.

For now, that’s for the equals() method, but we'll return to it a little bit later. Let's move on to hashCode().

hashCode()

hashCode() method returns a hash code value of the object. Hash code, for now, is some int value. And that's it.

Let’s try to see how it works to make it more clear. I propose to use our well-known Humanity class for this:

class Main {
public static void main(String[] args){
Humanity h = new Humanity("Linzhou");
int hashCode = h.hashCode();
System.out.println(hashCode);
}
}

The output will be different from several computers. But how could is it possibile ?
To answer this question, we need to remember that we have not override the hashCode() method yet, so we are using the Object implementation now. If we go to the Object class, we'll see the following:

public native int hashCode();

First, native in Java is used with methods to indicate, that the method is implemented in other languages (for example, C or C ++). It is possible to call such methods using JNI. Such methods are used for the performance reasons or to access system or hardware resources, that is not possible to proceed using Java.

Bref, let’s make this method behave predictably by overriding it:

@Override
public int hashCode() {
return 50;
}
Class Main {
public static void main(String[] args){
Humanity h = new Humanity("Linzhou");
int hashCode = h.hashCode();
System.out.println(hashCode);
}
}

As we might expect, the output is 50.

Let me present several hashCode() method rules and I hope we can understand why we need equals() combined with hashCode().

  1. Each invocation of hashCode() method on the same object that hasn't been changed must produce the same result each time.
  2. If two objects are equal through the equals() method, then invoking the hashCode() method on each of these two objects must produce the same result:
// if : 
a.equals(b) is true
//then :
a.hashCode() == b.hashCode()
  1. but if invoking the hashCode() method on two objects produces the same result, it doesn't mean that both of them are equal (through equals() method). Because, the int type has a limited scope of the values from -2147483648 to 2147483647, which means if we have a number of objects over than its limited scope and it will create a pair of hashCode with same values of two objects even if both of them are completely not the same type.

So as we have noticed, the 2nd and 3rd rules show that equals() and hashCode() are connected.

Let’s go deeper, hashed collections can provide fast access to elements (Time complexity O(1)). The most popular Java Hashed collections is HashMap. It is used to keep 'key - value' pairs.

Map<Integer, String> map = new HashMap<>();
map.put(1, "Hello");
map.put(2, "Bonjour");
map.put(3, "Hola");

If we need to get the third pair:

String str = map.get(3);
System.out.println(str);

The output is Hola.

When put(Key k, Value v) method is called, the following steps are performed under the hood:

  1. If key is null, the key-value pair (Entry) is put into the first bucket.
  2. If not null, the hashCode() method is called on the key.
  3. HashMap takes the key hash value, processes it through the internal computations to get the number of the bucket to be used.
  4. If the chosen bucket is empty, the Entry is out there.
  5. If the bucket is not empty, it iterates through all the existing entries inside the bucket and pares their keys with key from the put() method using equals().
  6. if equals() returns false for each iteration, the Entry will be stored in the current bucket.
  7. if equals() returns true for some case, so the Entry has the same key as from the put() method, this Entry value will be replaced by the new value.

When get(Key k) method is called:

  1. If key is null, we go to the first bucket and look for the Entry with the key == null, if exists, its value is returned.
  2. If key is not null, the hashCode() method is called on the key.
  3. hashMap takes the key hash value, processes it through the internal computations get get the number of the bucket to be used.
  4. If the bucket is empty, return null.
  5. if Bucket contains several elements, we iterate through each of them and compare equals() the key of each Entry with our key. If true return matching Entry value, else return null.

So, we have just seen that the equals() and hashCode() methods work together inside the HashMap to get and put the data: hashCode() is used to compute the bucket number and equals() is used to find the Entry with the same key.

Now we know more than enough to implement the hashCode() and equals() methods.

@Override
public int hashCode(){
return Objects.hash(idNumber);
}

And the final version :

public class Humanity {
private long idNumber;
private String name;
public Humanity(long idNumber, String name){
this.idNumber = idNmber;
this.name = name;
}
@Override
public boolean equals(Object o){
if (this == o)
return true;
if (o == null)
return false;
if (this.getClass() != o.getClass())
return false;
Humanity h = (Humanity) o;
return this.idNumber == h.idNumber;
}
@Override
public int hashCode() {
return Objects.hash(idNumber);
}
}

Thanks for reading my article. If you like it, please give me a small 👏. Appreciate ❤️

--

--