Implementing .equals

Brad Mongar Java, Opinion 6 Comments

Attention: The following article was published over 12 years ago, and the information provided may be aged or outdated. Please keep that in mind as you read the post.

Scenario

You are working on a project and you have two objects. You want to know if, according to the business, they are the same item. So you call .equals. It returns false because they aren’t the same object in memory. You then override the .equals method to compare the attributes that make the items equal to the business. This opens up a can of worms. 

Why?

.Equals is used by the system for many things– overriding it should not be taken lightly. If you have no reason to make HashMaps, or consider the items the same key, then you probably don’t want to override .equals. Instead, do something that makes sense to the business– for example, making a new method like .isTheSameAs.

I have seen many good programmers implement a bad .equals in Java. So what makes a good implementation of .equals? Well first, it has to meet the following contract:

  • The hashCode method must return the same integer value every time it is invoked on the same object during the entire execution of a Java application or applet. It need not return the same value for different runs of an application or applet. The Java 2 platform (Java 2) documentation further allows the hashCode value to change if the information used in the equals method changes.
  • If two objects are equal according to the equals method, they must return the same value from hashCode.
  • The equals method is reflexive, which means that an object is equal to itself: x.equals( x ) should return true.
  • The equals method is symmetric: If x.equals( y ) returns true, then y.equals( x ) should return true also.
  • The equals method is transitive: If x.equals( y ) returns true and y.equals( z ) returns true, then x.equals( z  should return true.
  • The equals method is consistent. x.equals( y ) should consistently return either true or false. The Java 2 javadoc clarifies that the result of x.equals( y ) can change if the information used in the equals comparisons change.
  • Finally, x.equals(null) should return false.

Most experienced programmers know the rules of .equals and .hashCode. However, they can implement something that meets the Java contract but still causes problems. In an attempt to make things better, the Eclipse IDE has a code assist that will generate a method that implements a .equals and a .hashCode that meet the contract. But, this makes the danger worse because programmers feel confident the implementation is safe.

Remember, just because a .equals and .hashCode implementation meets the contract doesn’t mean it is safe. If the attributes used in the .equals and .hashCode are mutable then the hashCode can, and should (according to the contract), change. Why is this so bad?  If the object is a key in a HashMap or HashTable and that object changes after being used as a key, the value it references is lost in the HashMap or HashTable.

Consider this example which uses the Eclipse generated .equals and .hashCode.

public class Name {
            private String firstName;
            private String lastName;
            private String middleInitial;
            @Override
            public boolean equals(Object obj) {
                        if (this == obj)
                                    return true;
                        if (obj == null)
                                    return false;
                        if (getClass() != obj.getClass())
                                    return false;
                        final Name other = (Name) obj;
                        if (firstName == null) {
                                    if (other.firstName != null)
                                                return false;
                        } else if (!firstName.equals(other.firstName))
                                    return false;
                        if (lastName == null) {
                                    if (other.lastName != null)
                                                return false;
                        } else if (!lastName.equals(other.lastName))
                                    return false;
                        if (middleInitial == null) {
                                    if (other.middleInitial != null)
                                                return false;
                        } else if (!middleInitial.equals(other.middleInitial))
                                    return false;
                        return true;
            }
            public String getFirstName() {
                        return firstName;
            }
            public String getLastName() {
                        return lastName;
            }
            public String getMiddleInitial() {
                        return middleInitial;
            }
            @Override
            public int hashCode() {
                        final int prime = 31;
                        int result = 1;
                        result = prime * result
                                                + ((firstName == null) ? 0 : firstName.hashCode());
                        result = prime * result
                                                + ((lastName == null) ? 0 : lastName.hashCode());
                        result = prime * result
                                                + ((middleInitial == null) ? 0 : middleInitial.hashCode());
                        return result;
            }
            public void setFirstName(String firstName) {
                        this.firstName = firstName;
            }
            public void setLastName(String lastName) {
                        this.lastName = lastName;
            }
            public void setMiddleInitial(String middleInitial) {
                        this.middleInitial = middleInitial;
            }

}
package evil.equals;

import java.util.Hashtable;

public class NameTest extends Name {

            public static void main(String[] args) {
                        Name aName = new Name();
                        aName.setFirstName("Brad");
                        aName.setMiddleInitial("K");
                        aName.setLastName("Mongar");
                        System.out.println("hash:" + aName.hashCode());

                        Hashtable<Name, String> aHashtable =  new Hashtable<Name, String>();
                        aHashtable.put(aName, "aValue");

                        aName.setFirstName("Bradley");
                        System.out.println("hash:" + aName.hashCode());

                        if (aHashtable.containsKey(aName)){
                                    System.out.println("found");
                        } else {
                                    System.out.println("not found");
                        }
             }
}

If you run the code you get an output of:

hash:603748753
hash:-455745749
not found

You can see that the value “value” is now stranded in the HashTable. This is something to think about before you override .equals. 

As a side note, you may also be thinking “Well, I’m not going to use the class as a key in a Hash*…” But keep in mind that objects are often used as keys in maps, persistence layers, or other architectural constructs you may not be aware of.

Simply put, my advice is that unless you make an object immutable, think twice or thrice before you implement a .equals.

–Brad Mongar, [email protected]

0 0 votes
Article Rating
Subscribe
Notify of
guest

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments