Interviewer: why do you have to rewrite hashCode when rewriting equals?

Important note: This is one of the bloggers' selected interview questions - Basic articles. Pay attention to me and view more interview questions. Gitee interview questions series open source address: https://gitee.com/mydb/interview

Difficulty: low

Common degree: high

The equals method and hashCode method are two basic methods in the Object class. They work together to determine whether two objects are equal. Why is it designed like this? The reason lies in the word "performance".

After using HashMap, we know that after hash calculation, we can directly locate the storage location of a value. Imagine if you want to query whether a value is in the collection now? If you do not directly locate elements (storage locations) through hash, you can only query and compare them one by one according to the sequence of the set, and the efficiency of this sequential comparison is significantly lower than that of hash positioning. This is the value of hash and hashCode.


When we compare whether two objects are equal, we can first use hashCode for comparison. If the comparison result is true, we can use equals to confirm whether the two objects are equal again. If the comparison result is true, the two objects are equal, otherwise the two objects are considered to be unequal in other cases. This greatly improves the efficiency of object comparison, which is why Java design uses hashCode and equals to confirm whether the two objects are equal.

Then why not use hashCode directly to determine whether two objects are equal?

This is because the hashcodes of different objects may be the same; However, objects with different hashcodes must not be equal, so using hashCode can quickly judge whether objects are equal for the first time.

However, even if you know the above basic knowledge, you still can't solve the problem in this article, that is, why do you have to rewrite hashCode when rewriting equals? To understand the root cause of this problem, we have to start with these two methods.

1.equals method

The equals method in the Object class is used to detect whether an Object is equal to another Object. In the Object class, this method will determine whether two objects have the same reference. If two objects have the same reference, they must be equal.

The implementation source code of equals method is as follows:

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

Through the above source code and the definition of equals, we can see that in most cases, the judgment of equals is meaningless! For example, using equals in Object to compare whether two custom objects are equal or not makes no sense at all (because the result is false regardless of whether the objects are equal or not).

This problem can be illustrated by the following example:

public class EqualsMyClassExample {
    public static void main(String[] args) {
        Person u1 = new Person();
        u1.setName("Java");
        u1.setAge(18);
        
        Person u2 = new Person();
        u1.setName("Java");
        u1.setAge(18);
        
        // Print equals results
        System.out.println("equals result:" + u1.equals(u2));
    }
}

class Person {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

The execution results of the above procedures are shown in the figure below:

Therefore, in general, if we want to judge whether two objects are equal, we must override the equals method, which is why we override the equals method.

2.hashCode method

hashCode is a hash code. It is an integer value derived from the object, and the value is any integer, including positive or negative numbers.

It should be noted that hash codes are irregular. If x and y are two different objects, x.hashCode() and y.hashCode() will not be the same basically; But if a and b are equal, a.hashCode() must be equal to b.hashCode().

The source code of hashCode in Object is as follows:

public native int hashCode();

As can be seen from the above source code, the hashCode in Object calls a native method and returns an integer of type int. of course, this integer may be positive or negative.

hashCode usage

An example where equal values hashCode must be the same:

public class HashCodeExample {
    public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "Hello";
        String s3 = "Java";
        System.out.println("s1 hashCode:" + s1.hashCode());
        System.out.println("s2 hashCode:" + s2.hashCode());
        System.out.println("s3 hashCode:" + s3.hashCode());
    }
}

The execution results of the above procedures are shown in the figure below:

Different values of hashCode may have the same example:

public class HashCodeExample {
    public static void main(String[] args) {
        String s1 = "Aa";
        String s2 = "BB";
        System.out.println("s1 hashCode:" + s1.hashCode());
        System.out.println("s2 hashCode:" + s2.hashCode());
    }
}

The execution results of the above procedures are shown in the figure below:

3. Why rewrite it together?

Now back to the topic of this article, why do you have to rewrite hashCode when rewriting equals?

To explain this problem, we need to start with the following example.

3.1 normal use

Set sets are used to save different objects. The same objects will be merged by set, leaving a unique piece of data.

Its normal usage is as follows:

import java.util.HashSet;
import java.util.Set;

public class HashCodeExample {
    public static void main(String[] args) {
        Set<String> set = new HashSet();
        set.add("Java");
        set.add("Java");
        set.add("MySQL");
        set.add("MySQL");
        set.add("Redis");
        System.out.println("Set Set length:" + set.size());
        System.out.println();
        // Print all elements in the Set
        set.forEach(d -> System.out.println(d));
    }
}

The execution results of the above procedures are shown in the figure below:

It can be seen from the above results that duplicate data has been "merged" by the Set set, which is also the biggest feature of the Set set: de duplication.

3.2 "exception" of set

However, if what we store in the Set collection is a custom object that only overrides the equals method, interesting things happen, as shown in the following code:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class EqualsExample {
    public static void main(String[] args) {
        // Object 1
        Persion p1 = new Persion();
        p1.setName("Java");
        p1.setAge(18);
        // Object 2
        Persion p2 = new Persion();
        p2.setName("Java");
        p2.setAge(18);
        // Create Set collection
        Set<Persion> set = new HashSet<Persion>();
        set.add(p1);
        set.add(p2);
        // Print all data in the Set
        set.forEach(p -> {
            System.out.println(p);
        });
    }
}


class Persion {
    private String name;
    private int age;

    // Only the equals method is overridden
    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Reference equality returns true
        // If it is equal to null or the object type is different, false is returned
        if (o == null || getClass() != o.getClass()) return false;
        // Force to custom person type
        Persion persion = (Persion) o;
        // Returns true if age and name are equal
        return age == persion.age &&
                Objects.equals(name, persion.name);
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
     @Override
    public String toString() {
        return "Persion{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

The execution results of the above procedures are shown in the figure below:

As can be seen from the above code and the above picture, even if the two objects are equal, the Set set does not de duplicate and merge them. This is the problem with rewriting the equals method but not the hashCode method.

3.3 resolve "exceptions"

To solve the above problem, we try to rewrite the hashCode method when rewriting the equals method. The implementation code is as follows:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class EqualsToListExample {
    public static void main(String[] args) {
        // Object 1
        Persion p1 = new Persion();
        p1.setName("Java");
        p1.setAge(18);
        // Object 2
        Persion p2 = new Persion();
        p2.setName("Java");
        p2.setAge(18);
        // Create Set object
        Set<Persion> set = new HashSet<Persion>();
        set.add(p1);
        set.add(p2);
        // Print all data in the Set
        set.forEach(p -> {
            System.out.println(p);
        });
    }
}


class Persion {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Reference equality returns true
        // If it is equal to null or the object type is different, false is returned
        if (o == null || getClass() != o.getClass()) return false;
        // Force to custom person type
        Persion persion = (Persion) o;
        // Returns true if age and name are equal
        return age == persion.age &&
                Objects.equals(name, persion.name);
    }

    @Override
    public int hashCode() {
        // Compare whether name and age are equal
        return Objects.hash(name, age);
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Persion{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

The execution results of the above procedures are shown in the figure below:

From the above results, we can see that when we rewrite the two methods together, a miracle happens again, and the Set set returns to normal. Why?

3.4 cause analysis

The reason for the above problem is that if only the equals method is rewritten, by default, Set will first judge whether the hashcodes of the two objects are the same when performing the de duplication operation. At this time, because the hashCode method is not rewritten, the hashCode method in the Object will be directly executed, and the hashCode method in the Object compares two objects with different reference addresses, Therefore, if the result is false, the equals method does not need to be executed. The direct return result is false: the two objects are not equal, so two identical objects are inserted into the Set set.

However, if the hashCode method is also rewritten when the equals method is rewritten, the rewritten hashCode method will be executed when the judgment is executed. At this time, the comparison is whether the hashcodes of all attributes of the two objects are the same, so the result returned by calling hashCode is true. Then call the equals method and find that the two objects are indeed equal, So, as like as two peas, true returned to Set, so the two sets of data were not stored in the collection.

summary

hashCode and equals are used to jointly judge whether two objects are equal. The reason for this method is that it can improve the speed of program insertion and query. If hashCode is not rewritten when equals is rewritten, program execution exceptions will occur in some scenarios, such as storing two similar custom objects in the Set set, In order to ensure the normal execution of the program, we need to rewrite the hashCode method when rewriting equals.

Pay attention to the official account: Java interview, real problem analysis, more Java interview questions.

Keywords: Java

Added by dstantdog3 on Fri, 03 Dec 2021 12:27:35 +0200