Requiring the argument to equals
to be of a specific concrete type is
incorrect, because it precludes any subtype of this class from obeying the
substitution principle.
Such code should be modified to use an instanceof
test instead of getClass
.
TL;DR: use composition rather than inheritance to add fields to value types.
The most common objection to this rule arises from a scenario like the following:
class Point {
double x() {...}
double y() {...}
}
class ColoredPoint {
double x() {...}
double y() {...}
Color color() {...}
}
It’s reasonable to think first of modeling ColoredPoint
as a subclass of
Point
. First, because on a conceptual level, there is a clear “is-a”
relationship between the two, which we have been taught to model as inheritance.
But also because of a few concrete advantages:
x
and y
fields/accessors for free.Point
for free.ColoredPoint
to anything that expects a Point
. (This is
by far the primary advantage, since the first two are just one-time-only
implementation helpers for the Point
authors themselves.)Although these same advantages can be achieved via composition, it’s no longer
quite “for free”; the frequent need to call myColoredPoint.asPoint()
is a pain
that feels unjustified, and thus subclassing is often chosen.
Unfortunately, equals
now creates a big problem. Two Points
should be seen
as interchangeable whenever they have the same x
and y
coordinates. But two
ColoredPoints
are only equivalent if their coordinates and color are the
same. This, plus the general contract of equals
(e.g. symmetry), ends up
forcing Point(1, 2)
to respond false
if asked whether it is equal to
ColoredPoint(1, 2, BLUE)
.
This can be achieved by changing equals
to be based on getClass
instead of
instanceof
. But do we even want to do that? Recall the main advantage we cited
for using subtyping: so that we “can pass ColoredPoint
to anything that
expects a Point
”, we said. Yet we are in fact not achieving that after all.
Put simply, ColoredPoint
is incapable of functioning properly as a Point
because it cannot participate in equality checks with other Point
s.
The composition approach may be annoying (the frequent need for .asPoint()
),
but aside from that annoyance, it at least achieves the three advantages
correctly.
In summation, while it is true conceptually that a ColoredPoint
“is-a”
Point
, a ColoredPoint
is nevertheless unable to properly function as a
Point
, and modeling it with subtyping is not a good idea for that reason.
In this case there is no disadvantage to the getClass
trick - but there’s no
great advantage to it either. Most unsafe idioms have circumstances in which
they are safe, but this doesn’t change the fact that they are generally unsafe
and not worth propagating and legitimizing.
See Effective Java 3rd Edition §10 (“Obey the general contract when overriding equals”).
Suppress false positives by adding the suppression annotation @SuppressWarnings("EqualsGetClass")
to the enclosing element.