Writing .equals()

Case Study: A Point Class

Let us consider the case of writing a class for keeping track of points in the plane with integer coördinates. Here is a bare-bones class for doing this. Note that it creates immutable objects. (You can add any getters you want) Place this in a file named Point.java.

public class Point
{
    private final int x;
    private final int y;
    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    public Point()
    {
        this(0, 0);
    }
    @Override
    public String toString()
    {
        return String.format("(%s, %s)", x, y);
    }
}
        

Uh Oh!

Compile your code using javac to make sure you have not typos. This being done, let's inspect it in jshell.

jshell> /open Point.java

jshell> Point p = new Point(3,4);
p ==> (3, 4)

jshell> Point q = new Point(3,4);
q ==> (3, 4)

jshell> p.equals(q)
$4 ==> false
        

Well this ain't fair! Our two object have identical state, yet Java says that they are unequal. This is because, by default, the .equals() method in Java tests for equality of identity. Python fans, this means that it is behaving just like Python's is operator.

This same scheme inheres in Python. If you do not implement the dunder method __eq__ in your class, the == operator behaves just like is. To check for equality of Points, we must implement a .equals() methods.

Common NOOB Mistake

To properly implement equals(), the correct signature is public boolean equals(Object o), not public boolean equals(Point p). If you use the highly-recommended @Override annotation, this error would be flagged by the compiler.

Implementation

Here are the key steps to implementing this method.

  1. If we are comparing ourself to ourself, return true.
  2. If we are comparing ourself to something that is not a Point, return false. This is called the species test.
  3. Once you are at this point, you are guaranteed that the object o is, in fact, a Point. Now cast it to a Point. Do not be afraid.
  4. Finally, check that the Point we are comparing ourselves to has the same coördinates that we have.

Let's go.

Step 1 compare the object to ourself. Notice that == in Java compares for equality of identity. So if you are the same as the object you are being compared to, return true.

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

Step 2 Perform the species test using the instanaceof operator. Notice the inner parentheses; they are needed.

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

Step 3 Perform the cast that is guaranteed to work.

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

Step 4 Check for equality of state.

    @Override
    public boolean equals(Object o)
    {
        if(this == o)
        {
            return true;
        }
        if((!o instanceof Point))
        {
            return false;
        }
        Point that = (Point) o;
        return x == that.x && y == that.y;
    }

Now let's inspect in jshell.

jshell> /open Point.java

jshell> Point p = new Point(3,4);
p ==> (3, 4)

jshell> Point q = new Point(3,4);
q ==> (3, 4)

jshell> Point r = new Point();
r ==> (0, 0)

jshell> p.equals(q)
$5 ==> true

jshell> p.equals(r)
$6 ==> false

jshell> p.equals("Hey I am a string
|  Error:
|  unclosed string literal
|  p.equals("Hey I am a string
|           ^

jshell> p.equals("Hey I am a string");
$7 ==> false

Why the Object o in the equals method?

Take a look in ArrayList's documentation. This is a parameterized (generic) class with type parameter E Your Point class can be used as an entry type like so.

ArrayList<Point> points = new ArrayList<>();

It has a method called contains. Here is the text from the API page.

public boolean contains(Object o) Returns true if this list contains the specified element. More formally, returns true if and only if this list contains at least one element e such that Objects.equals(o, e).

Specified by:
contains in interface Collection<E>

Specified by: contains in interface List<E>

Overrides: contains in class AbstractCollection<E>

Parameters: o - element whose presence in this list is to be tested

Returns: true if this list contains the specified element

The contains method uses the equals() method of the class of the entries E This method accepts as an argument an Object, not just an E.

A variable of type Object can point at any Java object. Take note of this jshell session.

jshell> /open Point.java

jshell> Point p = new Point(3,4);
p ==> (3, 4)

jshell> Object o = new Point(3,4);
o ==> (3, 4)


jshell> ArrayList<Point> points = new ArrayList<>();
points ==> []

jshell> points.add(p);
$6 ==> true

jshell> points.contains(o);
$7 ==> true

The complete program can be downloaded from the navigation area on the left.