Java's Object Methods: equals(Object)

Introduction

This article is a continuation of a series of articles describing the often forgotten about methods of the Java language's base Object class. The following are the methods of the base Java Object which are present in all Java objects due to the implicit inheritance of Object.

The focus of this article is the equals(Object) method which is used to test for equality among objects and gives the developer the ability to define a meaningful test of logical equivalence.

== vs equals(Object)

As you might have guessed the equals(Object) method is used to test for equality among reference types (objects) in Java. Ok, makes sense but, you might also be thinking "Why can't I just use ==?" The answer to this question is that when it comes to reference types the == operator is only true when comparing two references to the same instantiated object in memory. On the other hand the equals(Object) can be overridden to implement the notion of logical equivalence rather than mere instance equivalence.

I think an example would best describe this difference between using the == verse the equals(Object) method on Strings.

public class Main {  
    public static void main(String[] args) {
        String myName = "Adam";
        String myName2 = myName; // references myName
        String myName3 = new String("Adam"); // new instance but same content

        if (myName == myName2)
            System.out.println("Instance equivalence: " + myName + " & " + myName2);

        if (myName.equals(myName2))
            System.out.println("Logical equivalence: " + myName + " & " + myName2);

        if (myName == myName3)
            System.out.println("Instance equivalence: " + myName + " & " + myName3);

        if (myName.equals(myName3))
            System.out.println("Logical equivalence: " + myName + " & " + myName3);
    }
}

Output:

Instance equivalence: Adam & Adam  
Logical equivalence: Adam & Adam  
Logical equivalence: Adam & Adam  

In the example above I created and compared three String variables: myName, myName2 which is a copy of the reference to myName, and myName3 which is a totally new instance but with the same content. First I show that the == operator identifies myName and myName2 as being instance equivalent, which I would expect because myName2 is just a copy of the reference. Due to the fact that myName and myName2 are identical instance references it follows that they have to be logically equivalent.

The last two comparisons really demonstrate the difference between using == and equals(Object). The instance comparison using == demonstrates they are different instances with their own unique memory locations while the logical comparison using equals(Object) shows they contain the exact same content.

Diving into equals(Object)

Ok, we now know the difference between == and equals(Object), but what if I were to tell you the base implementation of the Object class actually produces the same result as the == operator?

What...!? I know... that seems strange, but hey the developers of Java had to start somewhere. Let me say that again, by default the equals(Object) method that you inherit in your custom classes simply tests for instance equality. It is up to us as the developers to determine if this is appropriate or not, that is, to determine if there is a notion of logical equivalence that is required for our class.

Again, let me use the Person class that I introduced previously in this series for more demonstration.

public class Person {  
    private String firstName;
    private String lastName;
    private LocalDate dob;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    // omitting getters and setters for brevity

    @Override
    public String toString() {
        return "<Person: firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">";
    }
}

Let me again use a simple program wrapped in a Main class that demonstrates both identical instance equality and logical equality by overriding equals(Object).

import java.time.LocalDate;

public class Main {  
    public static void main(String[] args) {
        Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
        Person me2 = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));

        if (me != me2)
            System.out.println("Not instance equivalent");

        if (!me.equals(me2))
            System.out.println("Not logically equivalent");
    }
}

Output:

Not instance equivalent  
Not logically equivalent  

As you can see the two people instances me and me2 are neither logically or instance equivalent out of the box, even though one would reasonably conceive that me and me2 represent the same thing based on the content.

This is where it becomes important to override the default implementation and provide one that makes sense for the class being defined. However, according to the official Java docs there are some rules that need to be followed when doing so to avoid problems with some important implementation dependencies of the language.

The rules outlined in the equals Java docs for given object instances x, y, and z are as follows:

  • reflexive: x.equals(x) must be true for all non-null reference instances of x
  • symmetric: x.equals(y) and y.equals(x) must be true for all non-null reference instances of x and y
  • transitive: if x.equals(y) and y.equals(z) then x.equals(z) must also be true for non-null reference instances of x, y, and z
  • consistency: x.equals(y) must always hold true where no member values used in the implementation of equals have changed in x and y non-null reference instances
  • no null equality: x.equals(null) must never be true
  • always override hashCode() when overriding equals()

Unpacking the Rules of Overriding equals(Object)

A. Reflexive: x.equals(x)

To me this is the easiest to grasp. Plus the default implementation of the equals(Object) method guarantees it, but for the sake of completeness I will provide an example implementation below that follows this rule:

class Person {  
    // omitting for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        return false;
    }
}

B. Symmetric: x.equals(y) and y.equals(x)

This one may seem intuitive at first glance, but it is actually quite easy to make a mistake and violate this rule. In fact, the main reason this is often violated is in cases of inheritance, which happens to be a very popular thing in Java.

Before I give an example let me update the equals(Object) method to account for the most obvious new requirement, which is the fact that the equivalence test must implement a logical test in addition to the instance equality test.

To implement a logical test I will want to compare the state-containing fields between two instances of the people class, described as x and y. In addition I should also check to make sure the two instances are of the same instance type, like so:

class Person {  
    // omitting for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Person)) {
            return false;
        }
        Person p = (Person)o;
        return firstName.equals(p.firstName)
                && lastName.equals(p.lastName)
                && dob.equals(p.dob);
    }
}

Ok, it should be evident that Person now has a much more robust equals(Object) implementation. Now let me give an example of how inheritance can cause a violation of symmetry. Below is a seemingly harmless class, called Employee, that inherits from Person.

import java.time.LocalDate;

public class Employee extends Person {

    private String department;

    public Employee(String firstName, String lastName, LocalDate dob, String department) {
        super(firstName, lastName, dob);
        this.department = department;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Employee)) {
            return false;
        }
        Employee p = (Employee)o;
        return super.equals(o) && department.equals(p.department);

    }
}

Hopefully you are able to notice that these should not be treated as equal instances, but you may be surprised with what I'm about to show you.

import java.time.LocalDate;

public class Main {  
    public static void main(String[] args) {
        Person billy = new Person("Billy", "Bob", LocalDate.parse("2016-09-09"));
        MinorPerson billyMinor = new MinorPerson(
                billy.getFirstName(),
                billy.getLastName(),
                billy.getDob());

        System.out.println("billy.equals(billyMinor): " + billy.equals(billyMinor));
        System.out.println("billyMinor.equals(billy): " + billyMinor.equals(billy));
    }
}

Output:

billy.equals(billyEmployee): true  
billyEmployee.equals(billy): false  

Oops! Clearly a violation of symmetry, billy equals billyEmployee but the opposite is not true. So what do I do? Well, I could do something like the following, given that I wrote the code and know what inherits what, then modify the Employee equals(Object) method like so:

import java.time.LocalDate;

public class Employee extends Person {

    private String department;

    public Employee(String firstName, String lastName, LocalDate dob, String department) {
        super(firstName, lastName, dob);
        this.department = department;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (instanceof Person && !(o instanceof Employee)) {
            return super.equals(o);
        }

        if (o instanceof Employee) {
            Employee p = (Employee)o;
            return super.equals(o) && department.equals(p.department);
        }

        return false;
    }
}

Output:

billy.equals(billyEmployee): true  
billyEmployee.equals(billy): true  

Yay I have symmetry! But am I really ok? Notice here how I'm going out of my way to make Employee now conform... this should be sending up a red flag which will come back to bite me later as I demonstrate in the next section.

C. Transitivity: if x.equals(y) and y.equals(z) then x.equals(z)

Thus far I have ensured that my Person and Employee classes have equals(Object) methods that are both reflexive and symmetric, so I need to check that transitivity is also being followed. I'll do so below.

import java.time.LocalDate;

public class Main {  
    public static void main(String[] args) {
        Person billy = new Person("Billy", "Bob", LocalDate.parse("2016-09-09"));
        Employee billyEngineer = new Employee(
                billy.getFirstName(),
                billy.getLastName(),
                billy.getDob(),
                "Engineering");
        Employee billyAccountant = new Employee("Billy", "Bob", LocalDate.parse("2016-09-09"), "Accounting");

        System.out.println("billyEngineer.equals(billy): " + billyEngineer.equals(billy));
        System.out.println("billy.equals(billyAccountant): " + billy.equals(billyAccountant));
        System.out.println("billyAccountant.equals(billyEngineer): " + billyAccountant.equals(billyEngineer));
    }
}

Output:

billyEngineer.equals(billy): true  
billy.equals(billyAccountant): true  
billyAccountant.equals(billyEngineer): false  

Darn! I was on such a good path there for a while. What happened? Well it turns out in classical inheritance within the Java language you cannot add an identifying class member to a subclass and still expect to be able to override equals(Object) without violating either symmetry or transitivity. The best alternative I have found is to use composition patterns instead of inheritance. This effectively breaks the rigid hierarchy of inheritance between the classes, like so:

import java.time.LocalDate;

public class GoodEmployee {

    private Person person;
    private String department;

    public GoodEmployee(String firstName, String lastName, LocalDate dob, String department) {
        person = new Person(firstName, lastName, dob);
        this.department = department;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Employee)) {
            return false;
        }

        GoodEmployee p = (GoodEmployee)o;
        return person.equals(o) && department.equals(p.department);
    }
}

D. Consistency: x.equals(y) as long as nothing changes

This one is really very easy to comprehend. Basically, if two objects are equal then they will only remain equal as long as neither of them change. Although this is easy to understand, caution should be taken to ensure that values do not change if there could be negative consequences resulting from such as change.

The best way to ensure things do not change in a class is to make it immutable by only supplying one way to assign values. Generally this one way on assignment should be via a constructor during instantiation. Also declaring class fields final can help with this.

Below is an example of the Person class defined as an immutable class. In this case two objects that are initially equal will always be equal because you cannot change their state once created.

import java.time.LocalDate;

public class Person {  
    private final String firstName;
    private final String lastName;
    private final LocalDate dob;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public LocalDate getDob() {
        return dob;
    }

    @Override
    public String toString() {
        Class c = getClass();
        return "<" + c.getSimpleName() + ": firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Person)) {
            return false;
        }
        Person p = (Person)o;
        return firstName.equals(p.firstName)
                && lastName.equals(p.lastName)
                && dob.equals(p.dob);
    }
}

E. No null equality: x.equals(null)

Sometimes you will see this enforced via a direct check for the Object instance o being equal to null, but in the above example this is implicitly checked using the !(o instanceof Person) due to the fact that the instanceof command will always return false if the left operand is null.

F. Always override hashCode() when overriding equals(Object)

Due to the nature of various implementation details in other areas of the Java language, such as the collections framework, it is imperative that if equals(Object) is overridden then hashCode() must be overridden as well. Since the next article of this series is going to specifically cover the details of implementing your own hasCode() method I will not be covering this requirement in any more detail here other than to say that two instances that exhibit equality via the equals(Object) method must produce the identical hash codes via hashCode().

Conclusion

This article described the meaning and use of the equals(Object) method along with why it may be important for your programs to have a notion of logical equality that differs from identity (instance) equality.

As always, thanks for reading and don't be shy about commenting or critiquing below.

Author image
Lincoln, Nebraska Twitter