Summary
Many Java programmers believe that they should include an instanceof test in equals methods. Many authors egg them on. This article explains why this is very wrong and muddleheaded in almost all cases, and urges programmers to test for equality of classes instead.
Advertisement
The Java Language Specification states that the equals method of any class
should be reflexive, symmetric (well, almost...null.equals(x) is different from x.equals(null)), and
transitive.
That's easy enough to achieve:
public Employee { public boolean equals(Object other) { if (getClass() != other.getClass()) return false; // cast other to Employee and compare fields . . . } }
What bugs me is how many programmers do something different and
dangerous, and how many authors egg them on. If you look at the
implementations of equals
in the source of the standard library (about 150), you will find that
the majority use an instanceof
test instead.
public Employee { public boolean equals(Object other) { if (!(other instanceof Employee)) return false; // cast other to Employee and compare fields . . . } }
This is a crummy idea. In many (most?) cases, a subclass will want
to override equals by
comparing the superclass fields and the subclass fields, like this:
public Manager extends Employee { public boolean equals(Object other) { . . . if (!super.equals(other)) return false; // cast other to Manager and compare fields return bonus == ((Manager)other).bonus; } }
But now equals has
become seriously asymmetric:
anEmployee.equals(aManager)
compares true if the
employee and manager have the same superclass data
aManager.equals(anEmployee)
throws an exception
That problem can be avoided easily. Does it ever make sense for an Employee to equal a Manager? No. Just forget about instanceof and test for equality
of classes instead.
Why don't more authors mention this?
For example, Josh Bloch has a chapter on equals in his Effective Java
book, and he never once mentions the getClass() != other.getClass()
test.
Instead, he pontificates about a rather pointless example with Point and ColoredPoint classes where equals does not take the point's colors into
consideration.
Why is there a stubborn refusal in the Java literature to recognize
the obvious?
You should almost always test for getClass() != other.getClass()
in an equals method
There is a small number
of cases where instanceof
makes sense, namely if
a superclass has fixed the semantics for equals
the state of various subclasses differ in inessential ways from
the state of the superclass
and the subclasses can add value by redefining equals to be more efficient
This is the case in the java.util.Collection hierarchy.
If a class is final it doesn't matter. But
there are lots of non-final classes with bad definitions of equals, for no better reason
than that the folks who should know better set a bad example.
The statement: if (getClass() != other.getClass()) return false; seems really dangerous. It is mixing the concept of identity with the concept of type. Why 2 objects of different types could not be equal?
> > Why 2 objects of different types could not be equal?
Two objects of different types cannot - safely - state that they are equal unless each of the classes is aware of how the other class determines equality. This implies that each class has knowledge of how the internals of the other class works. This has two bad implications. The first is that both classes are 'closely coupled' (i.e. changing the implementation of one class affects the way in which the other class is also implemented). The second is that the workings of one or other class is not properly encapsulated and may be being used by the other class in a way not intended by the developer of the class.
If two different objects have the same way of determining equality, that also implies that both those objects contain duplicate code or a common 'virtual object' within them. This duplicate code may need to be extracted into a third object, an instance of which would then be contained in the original objects.
2 improvements on the topic of Cay: - you will get a NullPointerException on other.getClass() if other is null - if 'this' and 'other' is a reference to the same object-instance, you can immediately return true
Thus...
public Employee{
publicboolean equals(Object other)
{
if (this == other) returntrue;
if (other == null) returnfalse;
if (getClass() != other.getClass()) returnfalse;
// cast other to Employee and compare fields
. . . }
}
> public Employee{
> publicboolean equals(Object other) {
> if (this == other) returntrue;
> if (other == null) returnfalse;
> if (getClass() != other.getClass()) returnfalse;
> // cast other to Employee and compare fields
> }
> }
This is a beautiful example of the use of guard clauses to simplify the code and generally make it clearer. Many programmers would have written something like:
publicboolean equals(Object other)
{
if (this == other) {
returntrue;
} elseif (other == null) {
returnfalse;
} elseif (getClass() != other.getClass()) {
returnfalse;
} else {
// cast other to Employee and compare fields
}
}
or even worse (in the misguided persuit of A Single Return):
publicboolean equals(Object other)
{
boolean result = false;
if (this == other) {
result = true;
} elseif (other == null) {
result = false;
} elseif (getClass() != other.getClass()) {
result = false;
} else {
// cast other to Employee and compare fields
}
return result;
}
If you happen to be using JUnit to write tests for you code, your can use EqualsTester that is part of Mike Bowler's Gargoyle JUnit Extensions. It tests for an equals method as descibed in Cay's post. It also tests for a correct implementation of hashCode().
> Why is there a stubborn refusal in the Java literature > to recognize the obvious?
Not all Java literature advocates the instanceof test. For instance, "Practical Java" by Peter Haggar gets it right and there have been some articles discussing the issue (see some reference below).
So tell me: Suppose an inheritance chain where Z extends Y extends X. The logic of X.equals() uses getClass(). In what way, exactly, is Y.equals() supposed to test if its X part is equal to the other object's X part?
It could reproduce the comparison subpart of X.equals, using accessible members of X, but that might lead to a different meaning of equality. This is a problem if, over time, the notion of X's equality changes (there are bugs, after all, even in designs, and these are some of them). Or worse, some parts of X's state may be purposefully not accessible.
If X's writer is thinking about this problem, X might have a protected X.contentEquals() method that does the comparison part of X.equals(), making it easy for Y.equals() to inherit that logic. But this will necessarily be a non-standard mechanism that each inventor of a class must (a) think about, (b) invent and name as makes sense to them, and (c) make available to subclass writers in a way that they will notice, as it is non-standard. I don't hear much cheering for this.
And then consider this with Z, who must replicate X and Y comparisons, and/or use X and Y comparison methods, as the writers of X and Y decided to make them.
Oy vey.
I personally think that a.equals(b) == b.equals(a) is one of those nice theories that really can't hold up as things are constituted. A real solution would be to have an "equivalent" operator distinct from an "identical" operator (which we have in ==). So one could say "a <-> b" or whatever, and it would invoke a.equals(b) after the null and class equivalence tests which must be universally applied. Then we could get somewhere. What we are given right now makes keeping the equivalence relationship a lot of work, and therefore distinctly unreliable.
In the universe we actually inhabit, I think that a dogmatic adherence to getClass vs. instanceof is overweening.
Why not let Manager decide whether it can be equal to an Employee like so:
Manager extends employee { boolean equals( Object other ) { if ( !super.equals( other ) ) return false; // Now I know my employee bits match... ... } }
Why does implementing equals acrosss classes have to break encapsulation?
... not advocating this, just seems like its a problem in the domain, not necessarily a hard fast rule.
> Why does implementing equals acrosss classes have to break > encapsulation? > > Pat Niemeyer
Encapsulation is the wrong word for what I meant. I'm sure someone here can supply the correct word for what I'm getting at.
Given instance a of class A and instance b of class B. To have A and B implement equals() such that a.equals(b) and b.equals(a) always agree, implies that they have corresponding code in their respective implementations. If class A is modified so that circumstances when a.equals(b) are changed, then the code in class B must be similarly modified (the interfaces of both classes being unaffected). Thus the implementations of A and B are dependent upon each having knowledge of how the other works. The equals criteria is not 'encapsulated' in a single place.
> Why not let Manager decide whether it can be equal to an > Employee...
(I guess I should have included this in my previous reply.)
If a Manager can decide if and when it can be equal to an Employee, then - to preserve symmetry - an Employee must also be able to decide when it can be equal to a Manager based on the same criteria. Now, Manager extends Employee so it has knowledge of the Employee class, however, the Employee class has no knowledge of the Manager class, so the Employee cannot guarantee that symmetry is maintained if the Manager class is modified.
Further, it could be that two Employees are equal if they have the same staff number. The same condition could apply to Managers. It would still be dangerous to allow a Manager to equal an Employee, since it implies the two classes are interchangeable. You would therefore come unstuck if you decided a given Employee was equal to a Manager and then treated the Employee as a Manager (perhaps by casting it or calling say e.makeDecision() when only Managers have a makeDecision() method.
The solution in such cases (one solution anyway) is to extract the commonality into a different objects and compare that. e.g. Manager still extends Employee but Employee contains a Person, leading to:
if ( m.getPerson().equals(e.getPerson()) )...
This would establish that both the Manager m and the Employee e are the same Person whilst avoiding the semantic confusion of implying that m and e are the same.
Objects of different *classes can* be equal. These are "true" for example: new HashSet().equals(new TreeSet())) new LinkedList().equals(new ArrayList())) new HashMap().equals(new TreeMap())
Although they are different classes each pair consists of objects of the same *type* (Set, List, Map). There is a difference (IMO) between types and classes. Each object is of exact one class, but might be of many types. That is the polymorphism - many forms.
Flat View: This topic has 28 replies
on 2 pages
[
12
|
»
]