Skip to content
On this page

The equals Method

We know that a List is an ordered collection: elements are stored in the order they were added, and each element can be accessed by its index.

The List interface also provides a boolean contains(Object o) method to check if the List contains a specific element. Additionally, the int indexOf(Object o) method returns the index of an element, or -1 if the element does not exist.

Let’s take a look at an example:

java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("A", "B", "C");
        System.out.println(list.contains("C")); // true
        System.out.println(list.contains("X")); // false
        System.out.println(list.indexOf("C")); // 2
        System.out.println(list.indexOf("X")); // -1
    }
}

Here, we should note an interesting question: Are the "C" we added to the List and the "C" we passed to contains("C") the same instance?

If these two "C"s are not the same instance, will the code still give the correct result? We can modify the code to test this:

java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("A", "B", "C");
        System.out.println(list.contains(new String("C"))); // true or false?
        System.out.println(list.indexOf(new String("C"))); // 2 or -1?
    }
}

Since we passed new String("C"), they are definitely different instances. The result still meets our expectations. Why is that?

This is because the List does not use == to determine if two elements are equal; it uses the equals() method. For example, the contains() method could be implemented like this:

java
public class ArrayList {
    Object[] elementData;
    public boolean contains(Object o) {
        for (int i = 0; i < elementData.length; i++) {
            if (o.equals(elementData[i])) {
                return true;
            }
        }
        return false;
    }
}

Therefore, to correctly use List methods like contains() and indexOf(), the instances added must correctly override the equals() method. This is why we can successfully use standard classes like String and Integer, as these classes already implement the equals() method properly.

Let's test this with a Person object:

java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Person> list = List.of(
            new Person("Xiao Ming"),
            new Person("Xiao Hong"),
            new Person("Bob")
        );
        System.out.println(list.contains(new Person("Bob"))); // false
    }
}

class Person {
    String name;
    public Person(String name) {
        this.name = name;
    }
}

As expected, even though we added new Person("Bob"), we cannot find it using another new Person("Bob"). This is because the Person class does not override the equals() method.

Writing equals

How do we correctly implement the equals() method? The equals() method must satisfy the following conditions:

  1. Reflexive: For any non-null reference x, x.equals(x) must return true.
  2. Symmetric: For non-null references x and y, if x.equals(y) returns true, then y.equals(x) must also return true.
  3. Transitive: For non-null references x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true.
  4. Consistent: For non-null references x and y, as long as x and y remain unchanged, x.equals(y) must consistently return true or false.
  5. Null comparison: x.equals(null) must always return false.

While these rules may seem complex, implementing the equals() method is actually quite straightforward. Using the Person class as an example:

java
public class Person {
    public String name;
    public int age;
}

First, we need to define the logical criteria for "equality." For the Person class, we consider two Person instances equal if their names are equal and their ages are equal.

Thus, the equals() method can be implemented as follows:

java
public boolean equals(Object o) {
    if (o instanceof Person p) {
        return this.name.equals(p.name) && this.age == p.age;
    }
    return false;
}

For comparing reference fields, we use equals(), and for comparing primitive type fields, we use ==.

If this.name is null, the equals() method will throw an error. Therefore, we need to modify it further:

java
public boolean equals(Object o) {
    if (o instanceof Person p) {
        boolean nameEquals = false;
        if (this.name == null && p.name == null) {
            nameEquals = true;
        }
        if (this.name != null) {
            nameEquals = this.name.equals(p.name);
        }
        return nameEquals && this.age == p.age;
    }
    return false;
}

If the Person class has several reference-type fields, the above implementation becomes too complex. To simplify reference type comparison, we can use the Objects.equals() static method:

java
public boolean equals(Object o) {
    if (o instanceof Person p) {
        return Objects.equals(this.name, p.name) && this.age == p.age;
    }
    return false;
}

Thus, we summarize the correct approach to implementing the equals() method:

  1. First, determine the logical criteria for instance "equality," i.e., which fields need to be equal for the instances to be considered equal.
  2. Use instanceof to check if the incoming object is of the current type; if so, proceed to compare; otherwise, return false.
  3. Use Objects.equals() for reference types and == for primitive types.

Using Objects.equals() to compare two reference types simplifies null checking. If both reference types are null, they are considered equal.

If you do not call List methods like contains() or indexOf(), then the elements added do not need to implement the equals() method.

Exercise

Add the equals method to the Person class so that calling the indexOf() method returns the expected result:

java
import java.util.List;
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        List<Person> list = List.of(
            new Person("Xiao", "Ming", 18),
            new Person("Xiao", "Hong", 25),
            new Person("Bob", "Smith", 20)
        );
        boolean exist = list.contains(new Person("Bob", "Smith", 20));
        System.out.println(exist ? "Test Successful!" : "Test Failed!");
    }
}

class Person {
    String firstName;
    String lastName;
    int age;
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}

Summary

When searching for elements in a List, the List's implementation compares elements using their equals() method. Therefore, the elements added must correctly override the equals() method. Standard classes provided by Java, like String and Integer, have already overridden the equals() method.

The equals() method can leverage Objects.equals() for comparison.

If you do not search for elements in a List, there is no need to override the equals() method.

The equals Method has loaded