例えば、座標を表すPoint
クラスに色情報を追加したColorPoint
クラスを継承で作ってはいけない。
何故ならば、Point.equals
とColorPoint.equals
をどう実装しても、
- 子クラスで追加した値要素を等値性の考慮対象に加える
- equalsの一般規約を守る
- リスコフの置換原則を守る
を同時に満たせないから。
詳細
Effective Javaの内容をまとめた【Effective Java】項目8:equals をオーバーライドするときは一般契約に従う - 王様の美術館の記事が参考になる。
下記の例も、上記リンクの内容の一部を自分用のメモとしてまとめ直しただけである。
例
座標を表すPoint
クラスに色情報を追加したColorPoint
クラスを作りたい。
ぱっと見ColorPoint
is-a Point
っぽいのでColorPoint
を継承で作っても良さそうな気がする。
しかし、ColorPoint.equals
が色情報を考慮した比較を行い、かつPoint.equals
とColorPoint.equals
がequalsの一般規約を守っている限り、リスコフの置換原則を守ることはできない。
例えばリスコフの置換原則に従えば、HashSet<Point>
にPoint
の代わりにColorPoint
を与えても動作は変化してはいけないはずだが、実際は動作が変化してしまう。
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class Point {
private final int x;
private final int y;
public Point(final int x, final int y) {
this.x = x;
this.y = y;
}
// Apache Commonsがドキュメントで推奨している書き方。
// equalsの一般規約を守ることはできる。
// https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/builder/EqualsBuilder.html 参照。
@Override
public boolean equals(final Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj.getClass() != this.getClass()) return false;
final Point rhs = (Point)obj;
return new EqualsBuilder()
.append(this.x, rhs.x)
.append(this.y, rhs.y)
.isEquals();
}
// equalsをオーバーライドした場合、hashCodeもオーバーライドしなければならない。
// https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/builder/HashCodeBuilder.html 参照。
@Override
public int hashCode() {
return new HashCodeBuilder(19, 61)
.append(this.x)
.append(this.y)
.toHashCode();
}
}
public enum Color {
RED,
GREEN,
BLUE,
}
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(final int x, final int y, final Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj.getClass() != this.getClass()) return false;
final ColorPoint rhs = (ColorPoint)obj;
return new EqualsBuilder()
.appendSuper(super.equals(obj))
.append(this.color, rhs.color)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(23, 67)
.appendSuper(super.hashCode())
.append(this.color)
.toHashCode();
}
}
このとき、
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class Main {
private static final Set<Point> map = Stream.of(new Point(0, 0)).collect(Collectors.toSet());
public static void main(final String[] args) {
System.out.println(map.contains(new Point(0, 0))); // true
}
}
の
System.out.println(map.contains(new Point(0, 0))); // true
を
System.out.println(map.contains(new ColorPoint(0, 0, Color.RED)));
に置き換えても出力は変わってはいけないはずだが、実際はtrue
でなくfalse
が出力される。
true
が出力されるようにPoint.equals
とColorPoint.equals
を書き換えようとすると、今度はequalsの一般規約を満たせなくなってしまう。(この辺りの詳細も【Effective Java】項目8:equals をオーバーライドするときは一般契約に従う - 王様の美術館に載っている)
解決策
そもそもColorPoint
はPoint
を置換不可能だったという話なので継承を使うのが間違い。
代わりに合成を使う。
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
public class ColorPoint {
private final Color color;
private final Point point;
public ColorPoint(final Color color, final Point point) {
this.color = color;
this.point = point;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj.getClass() != this.getClass()) return false;
final ColorPoint rhs = (ColorPoint)obj;
return new EqualsBuilder()
.append(this.color, rhs.color)
.append(this.point, rhs.point)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(23, 67)
.append(this.color)
.append(this.point)
.toHashCode();
}
}
格言
There is no way to extend an instantiable class and add a value component while preserving the equals contract, unless you are willing to forgo the benefits of object-oriented abstraction.
―――Effective Java (2nd Edition)