☰ Contents Extending Classes

Overriding equals and Casting to a Class

Student: Professor, the thing you showed me about equals() doesn’t work.
Prof: How do you mean?
Student: Well, I wanted to write some tests, like you suggested, and I tried using assertEquals with the class definition for Point.
Student: See, it fails! But if I make Point a record and get rid of equals, the assert passes.

Prof: Ah I see. Yeah this is a pretty subtle point! Here’s what’s going on. The default equals() on Object looks like this:

public boolean equals(Object other)

See the issue?

Student: No…
Prof: Let’s try two things. First, add an @Override on your equals method for Point.
Student: OK…
Student: Huh. It fails now. But I am overriding equals.

Prof: So this is where it’s a little tricky, and we need to be precise. We override a method not just by name, but by type. So let’s put the two methods right next to each other:

public boolean equals(Object other) // the one in Java's Object
public boolean equals(Point other)  // the one in your Point class
Student: So… how do I fix it? Do I have to write the Object version? OK, I can change the signature…
Student: Huh? It doesn’t work now
Prof: Right – since that parameter is type Object, Java can’t prove that it has x and y fields. You could call new Point(4.0, 5.0).equals("a string"), for example, and the string certainly doesn’t have those fields.
Student: Ugh, OK. Oh wait! I can use instanceof! Because I just want to do the x and y checks if I know it’s a Point!
Student: Ugh what’s wrong?
Prof: Wow, nice job. I just need to give you one more step. This is something that comes up in programming a lot – even though something is obvious to us, the programmer, it is not obvious to the programming language. Here you know that other must be a Point after the if. But Java doesn’t!
Student: Well that’s silly.
Prof: I know. But it’s actually a pretty common scenario. Here we need to tell Java “trust me, it’s a Point”. We can do that with a class cast. We write (C)o for some class C and value o. Java will make sure o instanceof
C
, and if it is, it will let us treat o as a C. If it’s not, that’s an error (a ClassCastException). So you could do that with other and Point.
Student: So like… where? Like after the if?
Prof: That’s the right spot, because it’s where you know you have a Point, and not some other subclass of Object like String or Location.
Student: Here goes…
Student: Hey, that works!
Prof: Right – this also helps fully explain what was going on in the equals for Raster. This is a super common pattern: do the instanceof check, do the cast, and then do the checks.
@Override
public boolean equals(Object other) {
    if (this == other) return true;
    if (!(other instanceof Raster r)) return false;
    if (this.width() != r.width() || this.height() != r.height()) return false;
    for (int y = 0; y < this.height(); y++) {
        if (!Arrays.equals(this.pixels[y], r.pixels[y])) return false;
    }
    return true;
}

Pict.java, lines 476–485

Student: But that has one more line, with == at the top.
Prof: Right, so if two objects are reference equal, they are certainly .equals(), so that check is also at the top of most equals() methods. You could add it to yours!
Student: Thanks, I will

Practice

Suppose Point’s equals skipped the instanceof check and cast right away:

@Override
public boolean equals(Object other) {
  Point p = (Point)other;
  return this.x == p.x && this.y == p.y;
}

What happens with new Point(4.0, 5.0).equals("a string")?

  1. It crashes with a ClassCastException
    The cast checks that other really is a Point when it runs. A String isn’t, so the program stops with a ClassCastException.
  2. It returns false
    Nothing in this version returns false for a non-Point. That was the job of the instanceof check.
  3. It doesn’t compile
    Java accepts the cast, because an Object might be a Point. The problem only shows up when the program runs.