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 Points.
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.