Why should the hashCode method be rewritten after the equals method is rewritten

Posted by thebopps on Wed, 26 Jan 2022 11:45:37 +0100

Let's first look at the source code of the equals method and hashcode method of the Object class:

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

From the code, we know that the created Object uses the equals method and hashcode method of Object without rewriting. From the source code of Object class, we know that the default equals determines whether the reference of two objects points to the same Object, and hashcode also generates an integer value according to the Object address.

Override the equals method

Suppose we create a Student class

public class Student {
	private Integer age;
	private String name;

	public Student() {
		
	}
	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}
	public static void main(String[] args) {

		Student student = new Student();

		student.setName("Zhang San");

		student.setAge(18);


		Student student2 = new Student();

		student2.setName("Zhang San");

		student2.setAge(18);

		System.out.println("student.equals(student2)=" + student.equals(student2));
	}

Run the above code and find two new Student() objects. Whether their values are the same or not, the two objects equals are always false.

The reason is that we did not override the equals method of Student. By default, we will call the equals method of Objec t. The equals method of Object is to compare whether the reference Object of the Object is the same. The two new objects must be different.

Now we need two objects with the same attribute values, so we think the two objects are equal; At this point, we need to override the equals method.

public class Student {
	private Integer age;
	private String name;

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Student) {
			Student user = (Student) obj;
			if (user.getName().equals(this.name) && user.getAge() == this.age) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}

	}
}
	public static void main(String[] args) {

		Student student = new Student();

		student.setName("Zhang San");

		student.setAge(18);


		Student student2 = new Student();

		student2.setName("Zhang San");

		student2.setAge(18);

		System.out.println("user1.equals(user2)=" + student.equals(student2));
	}

The final result will return true.

After we rewrite the equals method, the result is true. What if we put them into the Map collection respectively?

	public static void main(String[] args) {

		Student student = new Student();

		student.setName("Zhang San");

		student.setAge(18);

		Student student2 = new Student();

		student2.setName("Zhang San");

		student2.setAge(18);

		Map map = new HashMap();

		map.put(student, "student");

		map.put(student2, "student2");


		System.out.println("map The length of the is:" + map.keySet().size());
	}

Now the problem comes. It is clear that the two objects of student and student2 rewrite equals to true, so why there are two objects when they are put into the Map? The reason is that the Map regards two identical objects as different keys.

The reason for this problem is that the hashcode of student and student2 are different, because we do not override the hashcode method of the parent class (Object). The hashcode method of Object will generate corresponding hashcodes according to the addresses of the two objects.

Override hashcode method

Let's extend the above code and rewrite the hashcode method:

public class Student {
	private Integer age;
	private String name;

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Student) {
			Student user = (Student) obj;
			if (user.getName().equals(this.name) && user.getAge() == this.age) {
				return true;
			} else {
				return false;
			}
		} else {
			return false;
		}

	}

	@Override
	public int hashCode() {
		int result = name.hashCode();
		result = 31 * result + name.hashCode();
		result = 31 * result + age;
		return result;

	}
}

When we rewrite the hashcode method, the number in the map becomes 1.

Now that we have talked about HashMap, let's add a little to HashMap and further explain how equals and hashcode are implemented in HashMap.

HashMap related

jdk1. In 7, HashMap is composed of arrays and linked lists. How are objects stored?

put(K key, V value)

public V put(K key, V value) {  
    //If the table reference points to the member variable EMPTY_TABLE, then initialize HashMap
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //If the key is null, add the key value pair to table[0] and traverse the linked list. If the key is null, replace the value. If you don't create a new Entry object, put it in the header of the linked list
    //Therefore, a maximum of 1 Entry object is always stored in the position of table[0], and a linked list cannot be formed. An Entry with null key exists here 
    if (key == null)  
        return putForNullKey(value);  
    //If the key is not null, the hash value of the key is calculated
    int hash = hash(key);  
    //Search the index of the specified hash value in the corresponding table
    int i = indexFor(hash, table.length);  
    //Loop through the Entry object on the table array to determine whether the key already exists at this position
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        //The hash values are the same and the objects are the same
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            //If the key value pair corresponding to the key already exists, replace the old value with the new value, and then exit!
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    //Modification times + 1
    modCount++;
    //If there is no key value pair corresponding to the key in the table array, add the key value to table[i] 
    addEntry(hash, key, value, i);  
    return null;  
}

From the source code, we can see that when we pass the key and value to the put() method, HashMap will ask the key to call the hash() method, return the hash value of the key, and calculate the Index to find the location of the bucket (hash bucket) to store the Entry object.

If the hash values of the two object key s are the same, their bucket positions are the same, but the equals() is different. When adding elements, hash collision, also known as hash collision, will occur. HashMap uses a linked list to solve the collision problem.

By analyzing the source code, it can be seen that during put(), HashMap will traverse the table array first, and use hash value and equals() to judge whether there is exactly the same key object in the array. If the key object already exists in the table array, replace the old value with the new value. If it does not exist, create a new Entry object and add it to table [i].

If other elements already exist in the table [i], the new Entry object will be stored in the header of the bucket linked list and point to the original Entry object through next to form a linked list structure.

get(Object key)

 public V get(Object key) {
        //If the key is null, traverse the linked list at table[0] and take out the value with null key
        if (key == null)
            return getForNullKey();
        //If the key is not null, use the key to obtain the Entry object
        Entry<K,V> entry = getEntry(key);
        //If the Entry found in the linked list is not null, return the value in the Entry
        return null == entry ? null : entry.getValue();
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //Calculate the hash value of the key
        int hash = (key == null) ? 0 : hash(key);
        //Calculate the corresponding position of the key in the array and traverse the linked list of the position
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //If the key s are identical, the corresponding Entry object in the linked list is returned
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        //If the corresponding key is not found in the linked list, null is returned
        return null;
    }

From the source code, we can know that if the hashcodes of two different keys are the same and the two value objects are stored in the same bucket location, to obtain the value, we call the get() method. HashMap will use the hashcode of the key to find the bucket location. Because HashMap stores the Entry key value pair in the linked list, after finding the bucket location, The equals() method of key will be called to traverse each Entry in the linked list in order until the Entry to be obtained is found. If the Entry to be queried is at the end of the Entry chain, the HashMap must cycle to the end to find the element.

summary

From the above tests, we can conclude that:

If we need to implement a case like Student to judge whether two objects are equal based on object content, we will certainly rewrite the equals method in the object. In normal use, we just need to rewrite the equals method.

However, when it comes to putting objects into collections like HashMap and HashSet, their underlying principle is to first judge whether the hash value of the incoming key is the same. If it is different, it will be directly put into the collection. If it is the same, it will be judged by equals. If equals is also the same, the later incoming key will overwrite the previous key.

For wrapper classes such as String and Integer, the hashCode method has been overridden at the bottom, that is, they are unique. However, if we declare an object like Student ourselves, without overriding the hashCode method, the hash value of the value you pass in will be calculated first in the process of passing the object into the collection class. Because the object does not override the hashCode method, the object with the same content you put in twice will still be treated as two different objects. At this point, the only solution is to override the hashCode method in the object.

Topics: Java Back-end HashMap