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 classStudent: 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;
}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")?
- It crashes with a
ClassCastExceptionThe cast checks thatotherreally is aPointwhen it runs. AStringisn’t, so the program stops with aClassCastException. - It returns
falseNothing in this version returnsfalsefor a non-Point. That was the job of theinstanceofcheck. - It doesn’t compileJava accepts the cast, because an
Objectmight be aPoint. The problem only shows up when the program runs.