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

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

* 子クラスで追加した値要素を等値性の考慮対象に加える
* [equalsの一般規約](https://docs.oracle.com/javase/jp/7/api/java/lang/Object.html#equals%28java.lang.Object%29)を守る
* [リスコフの置換原則](http://d.hatena.ne.jp/asakichy/20090127/1233109959)を守る

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

# 詳細
Effective Javaの内容をまとめた[【Effective Java】項目８：equals をオーバーライドするときは一般契約に従う - 王様の美術館](http://hjm333.hatenablog.com/entry/2015/08/22/150006)の記事が参考になる。
下記の例も、上記リンクの内容の一部を自分用のメモとしてまとめ直しただけである。

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

```java: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();
    }
}
```

```java:Color.java
public enum Color {
    RED,
    GREEN,
    BLUE,
}
```

```java: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();
    }
}
```

このとき、

```java: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
    }
}
```

の

```java
        System.out.println(map.contains(new Point(0, 0))); // true
```

を

```java
        System.out.println(map.contains(new ColorPoint(0, 0, Color.RED)));
```

に置き換えても出力は変わってはいけないはずだが、実際は`true`でなく`false`が出力される。
`true`が出力されるように`Point.equals`と`ColorPoint.equals`を書き換えようとすると、今度はequalsの一般規約を満たせなくなってしまう。（この辺りの詳細も[【Effective Java】項目８：equals をオーバーライドするときは一般契約に従う - 王様の美術館](http://hjm333.hatenablog.com/entry/2015/08/22/150006)に載っている）

# 解決策

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

```java: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)