Chapter 6 - Java for Beginners Course

Object Equality - The Contract

As covered in the inheritance sections, we know that the base class of all classes in Java is the Object class.

One of the inherited methods from the Object class is the equals method that is used to check if the object is equal to an object provided as an input and has the following signature:

public boolean equals(Object obj);

This method should return true if this object is equal to the object provided as a parameter, or false otherwise.

It’s important to note that we’re talking about object equality here and not of object reference equality which was covered in the section about Equality and Relational Operators in Chapter 2.

The equals contract

The documentation of the equals method in the Object class defines a contract that should be followed by any classes that override it.

In summary, the contract specifies 5 main points, the first 3 being the same as the mathematical properties for equality:

  1. Reflective: For a non-null object x, x.equals(x) should return true (an object should be equal to itself).

  2. Symmetric: For 2 non-null objects x and y, x.equals(y) should return true if and only if y.equals(x) returns true.

  3. Transitive: For 3 non-null objects x, y and z, if x.equals(y) is true and y.equals(z) is true then x.equals(z) should return true.

  4. Consistent: For 2 non-null objects x and y, assuming their state hasn’t changed, multiple invocations of x.equals(y) should always return the same value.

  5. Not equal to null: For a non-null object x, x.equals(null) should return false.

The documentation also talks about the relationship with the hashCode method which will be covered in the next section.

equals example

For our example, let’s assume we are using a modified version of the DictionaryEntry class:

public class DictionaryEntry {
    private final String word;

    private final String definition;

    public DictionaryEntry(String word, String definition) {
        this.word = word;
        this.definition = definition;
    }

    public final void printEntry() {
        System.out.println(word + ": " + definition);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof DictionaryEntry)) {
            return false;
        }
        DictionaryEntry other = (DictionaryEntry) obj;
        return Objects.equals(word, other.word);
    }
}

You’ll notice that our equals method only uses the word field and not the definition as part of the equality checks. The decision on what makes two objects equal depends on the application you’re building, for our example we’ve decided that two DictionaryEntry objects are equal if they represent the same word.

Inside our equals method we are doing the following:

  • The first if statement above covers the reflective case.

  • The second if statement covers the not equal to null case.

  • The third if statement checks if the input object is of the same type, before we do final comparisons.

  • The last lines is where, after casting the input Object into our DictionaryEntry class, we start comparing the state of the objects to make a decision on equality. In our case, we check the word field (via the Objects.equals static method provided by Java).

Most IDEs nowadays provide a way to auto-generate an equals method that follows the required contract.
The Objects class provide a set of static utility methods to do common operations on objects.

Let’s give it a try:

In the example code run the JavaObjectEqualityPropertiesApp
DictionaryEntry run1 = new DictionaryEntry("run", "definition of run");

System.out.println("Reflective property:");
System.out.println("run1 equals itself: " + run1.equals(run1));

DictionaryEntry run2 = new DictionaryEntry("run", "definition of run");
System.out.println("Symmetric property:");
System.out.println("run1 equals run2: " + run1.equals(run2));
System.out.println("run2 equals run1: " + run2.equals(run1));

DictionaryEntry run3 = new DictionaryEntry("run", "definition of run");
System.out.println("Transitive property:");
System.out.println("run1 equals run2: " + run1.equals(run2));
System.out.println("run2 equals run3: " + run2.equals(run3));
System.out.println("run1 equals run3: " + run1.equals(run3));

System.out.println("Not equal to null:");
System.out.println("run1 equals null: " + run1.equals(null));

DictionaryEntry stop = new DictionaryEntry("stop", "definition of stop");
System.out.println("Not equal to different DictionaryEntry: ");
System.out.println("run1 equals stop: " + run1.equals(stop));

Output:

Reflective property:
run1 equals itself: true
Symmetric property:
run1 equals run2: true
run2 equals run1: true
Transitive property:
run1 equals run2: true
run2 equals run3: true
run1 equals run3: true
Not equal to null:
run1 equals null: false
Not equal to different DictionaryEntry:
run1 equals stop: false

Example explained

This is an example of the different properties of the equals method behaving as we expect them to. Here we are using a DictionaryEntry object run1 that represents the word "run" and we’re creating two additional objects run2 and run3 that also represent the same word to illustrate the properties of the equals contract.

We have 3 object references run1, run2 and run3 that point to 3 different objects, this is, the 3 references themselves are different, but the objects they point to are equal based on our application needs (in our case, they represent the same word).

We also create an object stop that represents a different word to illustrate that our equals method behaves as we expect it when we compare objects that represent different words.

However, there are common pitfalls when talking about object equality which we’ll cover in the next section.