Schoolhas either a name (“Stanford”) or a nickname (“UCSD”) or both.
Your task is to write
hashCode()methods for the
Schoolclass that satisfy the following requirement: two Schools are identical if they either have the same name or the same nickname.
I wrote EqualsVerifier; I should be able to do this! I came up with the following, which I thought was pretty ironclad:
I even added a
toString()! Obviously, I wasn’t really happy about the
hashCode() method, but I wasn’t really sure how to write it. Essentially, this implementation will turn your efficient hash collection with O(1) lookup into a list with O(n) lookup. So yeah, that’s pretty bad. But at least it meets the contract, so I thought I’d fix that later; first I wanted to see if my
equals() was correct. So I defined some tests with EqualsVerifier:
So far so good! I had to use the slightly verbose
forRelaxedEqualExamples() mode of EqualsVerifier here, because the regular case is meant for classes that are equal only when all their fields are equal, which is obviously not the case here: the equality relation defined by Beust is more relaxed – hence the name.
Then I decided to try and combine the two calls to EqualsVerifier:
And that’s where my test failed:
Precondition: not all objects are equal. And that’s true:
three aren’t equal to each other. But they should be, because the contract clearly states:
It is transitive: for any non-null reference values
So there you have it: this equality relation isn’t transitive. Therefore, it’s not possible to write an
equals() method that follows Beust’s requirement, and still meet the contract. Was the challenge a trick question?
This also explains why I had trouble with my
hashCode() method: equal objects must always return the same hashCode. So if
three are somehow equal, without having anything in common except the link with
two, then the only possible hashCode is a constant.
Now you might ask: so what if my
equals() method isn’t transitive? If this is the kind of equality semantics that I want, then why can’t I have it? It’s a free country!
And that’s true: you’re free to break the equals contract. But, as with any contract, if you break it, there will be consequences. In this case, the consequences will be subtle, hard-to-track bugs. One of the commenters had a good example of one such bug. Consider the following code:
Can you guess what that prints? Actually, you can’t, because it depends on the implementation of
HashSet. When you add a key to a
HashSet that’s already present in the set, the implementation can do two things. Either, it keeps the original one, in which case it will print:
Collection one has 2 elements. Collection two has 1 elements.
Or it replaces the old one with the new one, in which case the order will be reversed.
The moral of the story: writing a good
equals method is hard; don’t try to make it fancy.
If you want to have a non-transitive equality test, that’s fine, but don’t abuse
equals() to achieve that. There are other ways. One of them, as Beust points out in his follow-up post, is to add an
id field to the
School class and use that to base
hashCode() on. Another way could be to simply add another method to the class:
It even looks a lot better, because you don’t need all that type checking stuff anymore.
Just a few paragraphs ago I was saying how I needed to use EqualsVerifier’s
forRelaxedEqualExamples mode, because of the relaxed nature of this equality relation, and that the regular mode obviously wouldn’t work. I don’t know what made me decide to test that, anyway:
I’m going to have to fix that…