Java Challengers #4: Comparing Java objects with equals() and hashcode()

What is the contract between equals() and hashcode()? Learn how these methods work together when comparing Java objects

In this Java Challenger you’ll learn how equals() and hashcode() combine to make object comparisons efficient and easy in your Java programs. Simply put, these methods work together to verify if two objects have the same values.  

Without equals() and hashcode() we would have to create very large "if" comparisons, comparing every field from an object. This would make code really confusing and hard to read. Together, these two methods help us create more flexible and cohesive code.

Overriding equals() and hashcode() in Java

Method overriding is a technique where the behavior of the parent class or interface is written again (overridden) in the subclass in order to take advantage of Polymorphism. Every Object in Java includes an equals() and a hashcode() method, but they must be overridden to work properly.

To understand how overriding works with equals() and  hashcode(), we can study their implementation in the core Java classes. Below is the equals() method in the Object class. The method is checking whether the current instance is the same as the previously passed Object.


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

When the hashcode() method is not overridden, the default method in the Object class will be invoked. This is a native method, which means it will be executed in another language like C, and will return some code regarding the object's memory address. (It’s not that important to know exactly how this method works unless you are writing JDK code.)


@HotSpotIntrinsicCandidate
public native int hashCode();

When the equals() and hashcode() methods are not overridden, you will see the above methods invoked instead. In this case, the methods are not fulfilling the real purpose of equals() and hashcode(), which is to check whether two or more objects have the same values.

As a rule, when you override equals() you must also override hashcode().

Comparing objects with equals()

We use the equals() method to compare objects in Java. In order to determine if two objects are the same, equals() compares the values of the objects’ attributes:


public class EqualsAndHashCodeExample {

    public static void main(String... equalsExplanation) {
        System.out.println(new Simpson("Homer", 35, 120)
                 .equals(new Simpson("Homer",35,120)));
        
        System.out.println(new Simpson("Bart", 10, 120)
                 .equals(new Simpson("El Barto", 10, 45)));
        
        System.out.println(new Simpson("Lisa", 54, 60)
                 .equals(new Object()));
    }
	
    static class Simpson {

        private String name;
        private int age;
        private int weight;

        public Simpson(String name, int age, int weight) {
            this.name = name;
            this.age = age;
            this.weight = weight;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Simpson simpson = (Simpson) o;
            return age == simpson.age &&
                    weight == simpson.weight &&
                    name.equals(simpson.name);
        }
    }

}

In the first comparison, equals() compares the current object instance with the object that has been passed. If the two objects have the same values, equals() will return true.

In the second comparison, equals()checks to see whether the passed object is null, or if it’s typed as a different class. If it’s a different class then the objects are not equal.

Finally, equals() compares the objects’ fields.  If two objects have the same field values, then the objects are the same.

Analyzing object comparisons

Now, let’s view the results of these comparisons in our main() method. First, we compare two Simpson objects:


System.out.println(new Simpson("Homer", 35, 120).equals(new Simpson("Homer", 35, 120)));

The objects here are identical, so the result will be true.

Next, we compare two Simpson objects again:


System.out.println(new Simpson("Bart", 10, 45).equals(new Simpson("El Barto", 10, 45))); 

The objects here are nearly identical but their names are different: Bart and El Barto. Therefore the result will be false.

Finally, let’s compare a Simpson object and an instance of the class Object:


System.out.println(new Simpson("Lisa", 54, 60).equals(new Object())); 

In this case the result will be false because the class types are different.

equals() versus ==

At first glance, the == operator and equals() method may appear to do the same thing, but in truth they work differently. The == operator compares whether two object references point to the same object. For example:


System.out.println(homer == homer2);

In the first comparison, we instantiated two different Simpson instances using the new operator. Because of this, the variables homer and homer2 will point to different Object references in the memory heap. So we’ll have false as the result.

System.out.println(homer.equals(homer2));

In the second comparison, we override the equals() method. In this case only the names will be compared. Because the name of both Simpson objects is “Homer” the result will be true.

Uniquely identifying objects with hashcode()

We use the hashcode() method to optimize performance when comparing objects. Executing  hashcode() returns a unique ID for each object in your program, which makes the task of comparing the whole state of the object much easier.

If an object’s hashcode is not the same as another object’s hashcode, there is no reason to execute the equals() method: you just know the two objects are not the same. On the other hand, if the hashcode is the same, then you must execute the equals() method to determine whether the values and fields are the same.

Here’s a practical example with hashcode().


public class HashcodeConcept {

    public static void main(String... hashcodeExample) {
        Simpson homer = new Simpson(1, "Homer");
        Simpson bart = new Simpson(2, "Homer");

        boolean isHashcodeEquals = homer.hashCode() == bart.hashCode();

        if (isHashcodeEquals) {
            System.out.println("Should compare with equals method too.");
        } else {
            System.out.println("Should not compare with equals method because " +
                    "the id is different, that means the objects are not equals for sure.");
        }
    }

     static class Simpson {
        int id;
        String name;

        public Simpson(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Simpson simpson = (Simpson) o;
            return id == simpson.id &&
                    name.equals(simpson.name);
        }

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

A hashcode() that always returns the same value is valid but not very effective. In this case the comparison will always return true, so the equals() method will always be executed. There is no performance improvement in this case.  

Using equals() and hashcode() with collections

The Set interface is responsible for ensuring no duplicate elements will be inserted in a Set subclass. The following are some of the classes that implement the Set interface:

Only unique elements may be inserted into a Set, so if you want to add an element to the HashSet class (for example), you must first use the equals() and hashcode() methods to verify that the element is unique. If the equals() and hashcode()methods weren’t overridden in this case, you would risk inserting duplicate elements in the code.

In the code below, we’re using the add method to add a new element  to a HashSet object. Before the new element is added, HashSet checks to see whether the element  already exists in the given collection:


if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
       break;
       p = e; 

If the object is the same, the new element won’t be inserted.

Guidelines for using equals() and hashcode()

You should only execute an equals() method for objects that have the same unique hashcode ID. You should not execute equals() when the hashcode ID is different.

This principle is mainly used in Set or Hash collections for performance reasons.

Rules for object comparison

When a hashcode() comparison returns false, the equals() method must also return false. If the hashcode is different, then the objects are definitely not equal.

When the equals() method returns true, it means that the objects are equal in all values and attributes. In this case,  the hashcode comparison must be true as well.

Take the equals() and hashcode() challenge!

It’s time to test your skills with the equals() and hashcode() methods.  Your goal in this challenge is to figure out the output of the two equals() method comparisons and guess the size of the Set collection.

To start, study the following code carefully:


public class EqualsHashCodeChallenge {

    public static void main(String... doYourBest) {
        System.out.println(new Simpson("Bart").equals(new Simpson("Bart")));
        Simpson overriddenHomer = new Simpson("Homer") {
            public int hashCode() {
                return (43 + 777) + 1;
            }
        };

        System.out.println(new Simpson("Homer").equals(overriddenHomer));

        Set set = new HashSet(Set.of(new Simpson("Homer"), new Simpson("Marge")));
        set.add(new Simpson("Homer"));
        set.add(overriddenHomer);

        System.out.println(set.size());
    }

    static class Simpson {
        String name;

        Simpson(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object obj) {
            Simpson otherSimpson = (Simpson) obj;
            return this.name.equals(otherSimpson.name) &&
                    this.hashCode() == otherSimpson.hashCode();
        }

        @Override
        public int hashCode() {
            return (43 + 777);
        }
    }

}

Remember, analyze the code first, guess the result, and then run the code. Your goal is to improve your skill with code analysis and absorb core Java concepts to make your code more powerful. Choose your answer before checking the correct answer below.


A) 
true 
true 
4 

B) 
true 
false 
3 

C) 
true 
false 
2

D) 
false 
true 
3 

What just happened? Understanding equals() and hashcode()

In the first equals() method comparison, the result is true because the state of the object is exactly the same and the hashcode() method returns the same value for both objects.

In the second equals() method comparison, the hashcode() method is being overridden for the overridenHomer variable. The name is “Homer” for both Simpson objects, but the hashcode() method returns a different value for overriddenHomer. In this case, the final result from the the equals() method will be false because the method contains a comparison with the hashcode.

You might notice that the size of the collection is set to hold three Simpson objects. Let’s check this in a detailed way.

The first object in the set will be will be inserted normally:


new Simpson("Homer");

The next object will be inserted normally, as well, because it holds a different value from the previous object:


new Simpson("Marge");

Finally,  the following Simpson object has the same value as the first object. In this case the object won’t be inserted:


set.add(new Simpson("Homer"));

As we know, the overridenHomer object uses a different hashcode value from the normal Simpson(“Homer”) instantiation. For this reason, this element will be inserted into the collection:


overriddenHomer;

Common mistakes with equals() and hashcode()

  • Forgetting to override hashcode() along with the equals() method or vice versa.
  • Not overriding equals() and hashcode() when using hash collections like HashSet.
  • Returning a constant value in the hashcode() method instead of returning a unique code per object.
  • Using == and equals interchangeably. The == compares Object references, whereas equals() compares object values.

What to remember about equals() and hashcode()

  • It’s a good practice to always override equals() and hashcode() methods in your POJOs.
  • Use an effective algorithm to generate a unique hashcode.
  • When overriding the equals() method, always override the hashcode() method as well.
  • The equals() method should compare the whole state of objects: values from fields.
  • The hashcode() method could be the ID of a POJO.
  • When the result of comparing two object's hashcodes is false, the equals() method should also be false.
  • If equals() and hashcode() are not overridden when using hash collections, the collection will have duplicate elements.

Learn more about Java