banner
lMingyul

lMingyul

记录穿过自己的万物
jike

Are these 2 objects the same?

When writing business code, we often need to determine whether 2 Java objects are the same. Common methods for checking object equality include ==, equals, and hashcode. This article attempts to clarify the usage of these three methods.

Relational Operator ==#

Using == to determine if 2 objects are equal checks whether the addresses of these 2 objects are the same.

Example code:

public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        Person tom = new Person();
        Person bob = jack;

        System.out.println("jack == tom ? " + (jack == tom));
        System.out.println("jack == bob ? " + (jack == bob));
      
        System.out.println("jack address: " + jack);
        System.out.println("tom address: " + tom);
        System.out.println("bob address: " + bob);
    }
}

Output:

jack == tom ? false
jack == bob ? true
  
jack address: com.mingyu.javalearn.Person@1b2c6ec2
tom address: com.mingyu.javalearn.Person@4edde6e5
bob address: com.mingyu.javalearn.Person@1b2c6ec2

It can be seen that although jack and tom are both newly created Person objects, the addresses of the newly created Person objects are different, so jack is not equal to tom. From the printed address results, it can be seen that jack and bob have the same object address, so their comparison result is true.


Equals() Method#

In Java, all classes have a parent class called Object, and every class inherits the methods of this class, including the equals() method.

The Essence of Equals()#

After inheriting the parent's method, subclasses can override the same-named method of the parent class. If the equals() method is not overridden, using the equals method to determine if 2 objects are equal yields the same result as using the == operator mentioned above, both check whether the addresses of these 2 objects are the same.

Example code:

public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        Person tom = new Person();
        Person bob = jack;

        System.out.println("jack == tom ? " + (jack.equals(tom)));
        System.out.println("jack == bob ? " + (jack.equals(bob)));
    }
}

Output:

jack == tom ? false
jack == bob ? true

If we look at the underlying source code of the equals() method, it actually uses == to compare the 2 objects.

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

Overriding the equals() Method#

Sometimes we need to compare not only whether the addresses of 2 objects are the same but also whether the contents of these 2 objects are the same. In this case, we need to override the equals method.

The equals method can be overridden as follows:

@Override
public boolean equals(Object o) {
    // Check if it is the same object's reference, if so, return true directly
    if (this == o) {
      return true;
    }
    // Check if it is the same type, if not, return false directly
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    // Cast the Object to Person
    Person person = (Person) o;
    // Check if the contents of the objects are equal, here we check the content of name
    return Objects.equals(name, person.name);
}

This way, the judgment of the objects will be different; this time it checks whether the contents of the objects are the same.

public class Test {

    public static void main(String[] args) {
        Person jack = new Person("jack");
        Person tom = new Person("jack");

        System.out.println("jack == tom ? " + (jack.equals(tom)));
        System.out.println("jack address: " + jack);
        System.out.println("tom address: " + tom);
    }
}

Output:

jack == tom ? true
jack address: com.mingyu.javalearn.Person@1b2c6ec2
tom address: com.mingyu.javalearn.Person@4edde6e5

It can be seen that although the object instances of jack and tom have different object addresses, since we have overridden the equals() method, the focus of the comparison is on the contents of the objects, so the comparison result is true.

Other Overriding Methods for equals()#

Many other well-packaged libraries have overridden the equals() method.

Apache Commons Lang Framework#

@Override
public boolean equals(Object o) {
    // Check if it is the same object's reference, if so, return true directly
    if (this == o) {
      return true;
    }
    // Check if it is the same type, if not, return false directly
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    // Cast the Object to Person
    Person person = (Person) o;
    // Check if the contents of the objects are equal, here we check the content of name
    return new EqualsBuilder().append(name, person.name).isEquals();
}

The Difference Between equals and ==#

  • For primitive types, == checks whether the two values are equal; primitive types do not have an equals() method.
  • For reference types, == checks whether the two variables reference the same object, while equals() behaves the same as == when not overridden, and checks whether the contents of the referenced objects are the same after being overridden.

hashCode() Method#

hashCode is also a method defined in the Object class, which can compare whether two objects are equal. The return value of the method is the hash value of the object, which is of type int.

Implementation of hashCode()#

Looking at the source code of the Object class, we can find that the hashCode() method does not have a specific implementation because it is a native method implemented in C language. The hash value returned by this method is obtained by converting the memory address of the object into an integer.

public native int hashCode();

Why Do We Need the hashCode() Method?#

As mentioned above, the equals method can determine whether objects are equal. Why do we still need the hashCode method?

The reason is mentioned in the comments of the source code:

  • This method supports hash tables, such as the hash table provided by java.util.HashMap.

We know that collection classes like HashSet and HashMap perform an operation when adding elements to the collection: check whether the object to be added to the collection is already stored in the current collection. This involves comparing objects.

Both HashSet and HashMap use the hashCode() method to calculate where the object should be stored. Therefore, before adding an object to these collection classes, the hashCode value of the key to be stored must be obtained.

Here is the method for obtaining the hashCode value of the key in the HashMap source code:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

So why use the hashCode method instead of the equals method?

The reason is that calling the equals method is more time-consuming.

The following experiment was conducted:

public class Test {

    public static void main(String[] args) {
        // Start time
        long stime = System.currentTimeMillis();

        // Calculate execution time
        Person jack = new Person();
        Person tom = new Person();
        System.out.println("jack == tom ? " + (jack.equals(tom)));
        
        // End time
        long etime = System.currentTimeMillis();
        System.out.printf("equals() execution duration: %d milliseconds.", (etime - stime));

        System.out.println();
        System.out.println("========== Obvious Divider =========");
        
        // Start time
        stime = System.currentTimeMillis();

        // Calculate execution time
        Person mingyu = new Person();
        Person bob = new Person();
        System.out.println("mingyu == bob ? " + (mingyu.hashCode() == bob.hashCode()));

        // End time
        etime = System.currentTimeMillis();
        System.out.printf("hashCode() execution duration: %d milliseconds.", (etime - stime));
    }
}

Output:

jack == tom ? true
equals() execution duration: 3 milliseconds.
========== Obvious Divider =========
mingyu == bob ? false
hashCode() execution duration: 0 milliseconds.

From the output, it can be seen that calling the hashCode method takes almost no time because it essentially compares 2 int values, so using the hashCode method to compare two objects is very fast.

Why Do We Need the equals() Method?#

Since the hashCode method is so fast, why do we still need the equals method? This is because the hashCode method has limitations.

Limitation: Two objects having the same hashCode value does not mean the two objects are equal.

This is because the hashCode value is calculated using a hash function, and the general calculation process involves taking the modulus of the array length. This array can be a memory array or a collection array. Since the length is limited, it is difficult to avoid different hash values being calculated each time, leading to "hash collisions." The worse the hash algorithm, the more likely collisions will occur.

Hash collisions affect the comparison of whether two objects are equal, meaning that two different objects can also have the same hash value.

Therefore, relying solely on the hashCode method is not enough; the equals method is also needed for further judgment.

HashSet uses both of these methods when adding elements to the collection.

  • Steps for adding elements:
    • Calculate the hashCode value of the object to determine where to add the object.
    • Compare the hashCode values with other already added objects.
      • If there are no matching hashCode values, it proves that the object is not in the current collection because the hashCode values of two equal objects must be the same.
      • If there is a matching hashCode value, further judgment is made using the equals method.
    • If the object is not in the current collection, it is stored in the collection.

This process intercepts many existing elements in the collection from calling the equals method, improving the execution speed of the program.

The Relationship Between equals and hashCode#

In Effective Java, it is stated:

Whenever you override equals, you must also override hashCode.

In every class that overrides the equals method, the hashCode method must also be overridden.

If you only override equals without overriding hashCode, problems will arise when objects of this class are added as elements to hash-based collections (including HashMap, HashSet, and Hashtable).

Example code:

public class Test {

    public static void main(String[] args) {

        Person person1 = new Person("jack");
        Person person2 = new Person("jack");
        HashSet<Person> set = new HashSet<>();
        set.add(person1);
        set.add(person2);

        System.out.println("person1 equals person2: " + person1.equals(person2));
        System.out.println("person1.hashCode == person2.hashCode: " + (person1.hashCode() == person2.hashCode()));
        System.out.println("person1 hashCode: " + person1.hashCode());
        System.out.println("person2 hashCode: " + person2.hashCode());

        System.out.println("Number of elements in set: " + set.size());
        for (Person person : set) {
            System.out.println("person: " + person.getName());
        }
    }
}

Output:

person1 equals person2: true
person1.hashCode == person2.hashCode: false
person1 hashCode: 455896770
person2 hashCode: 1323165413
Number of elements in set: 2
person: jack
person: jack

Since the hashCode method was not overridden, every time an object is created, the hashCode method of the Object class is called, which generates different hash values.

As mentioned earlier, HashSet determines whether an object exists in the current collection based on the object's hash value, which leads to the results of the equals method and hashCode comparison being different, causing HashSet to contain 2 objects that we consider to be duplicates.

Therefore, when overriding the equals method, it is essential to also override the hashCode method.

How should the hashCode method be overridden?

  • Principles to follow when overriding:
    • If the information used in the equals method comparison of the object has not been modified, then multiple calls to the hashCode method of this object must always return the same value.
    • If 2 objects are equal according to the equals method, then the hashCode method of both objects must return the same result.
public class Person {

    private String name;

    @Override
    public boolean equals(Object o) {
        // Check if it is the same object's reference, if so, return true directly
        if (this == o) {
            return true;
        }
        // Check if it is the same type, if not, return false directly
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        // Cast the Object to Person
        Person person = (Person) o;
        // Check if the contents of the objects are equal, here we check the content of name
        return new EqualsBuilder().append(name, person.name).isEquals();
    }

    @Override
    public int hashCode() {
      // Return the hash value of the name field
        return Objects.hash(name);
    }
}

Output:

person1 equals person2: true
person1.hashCode == person2.hashCode: true
person1 hashCode: 3254270
person2 hashCode: 3254270
Number of elements in set: 1
person: jack

After overriding, the hashCode values of the two objects are the same, and the number of elements in the set is also the expected result: 1 element.


References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.