Why all equals() are not created equally
Since I’ve been testing software for a few years now I’ve got a certain soft spot for equals methods in Java. How could I not? Even if your program isn’t checking for the equality of objects as a part of its normal processes, it’s essential for your tests to be checking; otherwise you’ll be running into problems down the road when your code isn’t running the way you expected.
So it’s a little weird that getting equals() right is a bit unintuitive. Checking for equality sounds so straightforward, what could possibly go wrong?
Problem: Not writing equals() at all
Of course the first thing that could go wrong is not doing anything at all. Even without writing an equals method for a class your code will compile and run without shouting any errors at you (and I’ll explain why in a bit). That might seem like a handy built in feature, but take a look at this:
public class EqualityTest {
private static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
} public static void main(String[] args) {
Person erin = new Person("Erin", 38);
Person mark = new Person("Mark", 25);
Person anotherErin = new Person("Erin", 38);
System.out.println(erin.equals(erin));
System.out.println(erin.equals(mark));
System.out.println(erin.equals(anotherErin));
}
}
In the EqualityTest class here we’ve got a private Person class with name and age fields. When the program runs we create three of those Person objects (erin, mark, and anotherErin), and we print out the results of equality checks on them. What we haven’t added is an equals method, but again, that won’t prevent the code from running, so go ahead and run it. Really, paste that thing in and run it. I’ll wait.
What we see first on the console when we run this code is that erin.equals(erin) returns true. That’s good — we’ve achieved reflexivity! And just a reminder: if erin were null we’d hit a NullPointerException for trying to invoke a method on it. Don’t do that! Check for nulls when applicable (and when you’re not trying to be concise for a blog)!
Next up we see that erin.equals(mark) returns false. Another good sign — erin and mark have completely different names and ages, so we want to see false here.
Last but not least we see that erin.equals(anotherErin) returns…false? Even though they have the same name and age, they’re evaluating as unequal. As it turns out, erin and anotherErin are unequal for the same reason erin and mark were: the equals method is actually only checking to see if the references are the same, not that there’s any sort of equality between the state of the objects they’re referencing. That’s because we’re inheriting equals() from the Object class (all objects will!), which explains why the code compiled even though we never wrote an equals method. And equals() from the Object class is really strict — check out this blurb from the Oracle docs:
The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).
That’s not terribly useful for our use case, is it? So now that we know we aren’t going to get anywhere with Object’s equals method, we should add our own. Get ready for our next stumbling block!
Problem: Overloading equals()
We want equals() to return true for erin.equals(erin), false for erin.equals(mark), and true for erin.equals(anotherErin). Using the equals method inherited from the Object class really only got us 1/3 of the way there (maybe 2/3 if you don’t mind that it came to the correct conclusion with incorrect logic for erin.equals(mark)). So how would we go about writing our own version? Let’s break down what we’ll need.
First off we know that our version of equals() will need to return a boolean value. That boolean value should come about as a result of comparing the state of our two Person objects, so that comparing two Persons with the same name and age returns true and comparing two Persons with any differences in name and age returns false. Easy peasy, here it is with the new method bolded along with an additional test of equality:
public class EqualityTest {
private static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public boolean equals(Person other) {
return (name.equals(other.name)) && age == other.age;
}
} public static void main(String[] args) {
Person erin = new Person("Erin", 38);
Person mark = new Person("Mark", 25);
Person anotherErin = new Person("Erin", 38);
Object yetAnotherErin = new Person("Erin", 38); System.out.println(erin.equals(erin));
System.out.println(erin.equals(mark));
System.out.println(erin.equals(anotherErin));
System.out.println(erin.equals(yetAnotherErin));
}
}
*Note the use of the String class’ equals method inside our Person class’ equals method — thanks for providing that one, Java! This could have gotten really Inception-y really fast.
Go ahead and run this version of the code. First up is erin.equals(erin), which still returns true. Then comes erin.equals(mark), which returns false (and for the right reason this time). This version even beats out the last version by returning true for erin.equals(anotherErin), recognizing that even though those references point to different objects the objects have the same state. Success!
Oh, except for that last test we added in. Instantiating a third version of erin as an Object (because a Person is an Object, thus the inheritance from before) and comparing it to the first erin returns false. What gives?!
We were so close, but we made a bad assumption: that Person objects will only need to be compared to other Person objects. In this code the first three tests are using the new equals method we wrote because we’re passing a Person to the method, but in the final test it’s defaulting to the Object class’ version of equals() because we’re passing an Object. We overloaded equals() instead of overriding it, so there are two versions of it hanging around in our code. This may seem like a silly problem in this tiny, abstract example, but in a large codebase you’ll see superclass/subclass relationships everywhere and new code will need to be able to handle that.
That brings us to our final version of equals().
Solution: Overriding equals()
We weren’t far off on our last version of equals(), we just need to make a few adjustments. We now know that we need to write our method so that it takes an Object in order to work in all superclass/subclass situations, which will mean making a few more tweaks inside the method. Our final version, again with the new version of equals() and new test content bolded:
public class EqualityTest {
private static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object other) {
if (other instanceof Person) {
Person castOther = (Person) other;
return (name.equals(castOther.name)
&& age == castOther.age);
} else {
return false;
}
}
} private static class Professional extends Person {
private String profession;
public Professional(String name, int age, String profession) {
super(name, age);
this.profession = profession;
}
} public static void main(String[] args) {
Person erin = new Person("Erin", 38);
Person mark = new Person("Mark", 25);
Person anotherErin = new Person("Erin", 38);
Object yetAnotherErin = new Person("Erin", 38);
Professional devErin = new Professional("Erin", 38, "Dev"); System.out.println(erin.equals(erin));
System.out.println(erin.equals(mark));
System.out.println(erin.equals(anotherErin));
System.out.println(erin.equals(yetAnotherErin));
System.out.println(erin.equals(devErin));
}
}
Our first three tests come out correct as before, and now the fourth one does as well. The reason that fourth one is correct at this point is because the equals method recognizes the superclass connection of Object and Person by using instanceof. This is an easy way to narrow down which objects make sense to bother comparing (and which would even allow for proper casting like we do in the next line).
This works for the subclasses too, as the fifth test we added shows. The new Professional class is a subclass of Person with an additional String field for profession. Even with that difference in state, by calling equals() on a Person it will compare based on name and age since that’s all Person accounts for. If we called equals() on our Professional object instead it would still come as true because Professional inherited Person’s equals method, but we would override that method as well if we needed to.
tl;dr
There are always going to be situations where more complicated solutions are needed, but for general purposes this is a great place to start on equals methods:
- Override Object’s equals method by using the same method name and accepting an Object as the parameter
- Check for superclass/subclass connections with instanceof
- Cast to the appropriate class and compare meaningful parts of state
Of course since we went to the trouble to override equals() we also need to override hashCode() if we don’t want to make a mess of things, but that’s a story for another time. Best of luck with all those object comparisons!