LoginSignup
24
23

More than 5 years have passed since last update.

[Java] 値を表す具象クラスに値要素を追加したクラスを継承で作ってはいけない

Last updated at Posted at 2016-01-19

例えば、座標を表すPointクラスに色情報を追加したColorPointクラスを継承で作ってはいけない。
何故ならば、Point.equalsColorPoint.equalsをどう実装しても、

を同時に満たせないから。

詳細

Effective Javaの内容をまとめた【Effective Java】項目8:equals をオーバーライドするときは一般契約に従う - 王様の美術館の記事が参考になる。
下記の例も、上記リンクの内容の一部を自分用のメモとしてまとめ直しただけである。

座標を表すPointクラスに色情報を追加したColorPointクラスを作りたい。
ぱっと見ColorPoint is-a PointっぽいのでColorPointを継承で作っても良さそうな気がする。
しかし、ColorPoint.equalsが色情報を考慮した比較を行い、かつPoint.equalsColorPoint.equalsがequalsの一般規約を守っている限り、リスコフの置換原則を守ることはできない。
例えばリスコフの置換原則に従えば、HashSet<Point>Pointの代わりにColorPointを与えても動作は変化してはいけないはずだが、実際は動作が変化してしまう。

Point.java
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();
    }
}
Color.java
public enum Color {
    RED,
    GREEN,
    BLUE,
}
ColorPoint.java
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();
    }
}

このとき、

Main.java
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.equalsColorPoint.equalsを書き換えようとすると、今度はequalsの一般規約を満たせなくなってしまう。(この辺りの詳細も【Effective Java】項目8:equals をオーバーライドするときは一般契約に従う - 王様の美術館に載っている)

解決策

そもそもColorPointPointを置換不可能だったという話なので継承を使うのが間違い。
代わりに合成を使う。

ColorPoint.java
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)

24
23
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
23