Javaで独自クラスのequals, hashCodeをIDEを使って生成したら罠にはまった

# 前提条件
- IntelliJのversionは2019/09/13現在の最新版2019.2

```
$ java -version
java version "10.0.2" 2018-07-17
```
# ことのなりゆき
Javaで独自のクラスを定義したときに、`equals`や`hashCode`をきちんとoverrideしないともろもろの独自のクラスのコレクションがきちんと動作しなくなる。

今回、シンプルな独自クラスを定義して、`equals`や`hashCode`をIntelliJというIDEを用いて生成したところ、どハマりしたため共有する。

- 最初に自分が書いていたコード: `DistanceToNode`という独自のクラスを定義し、そのクラスについて以下のような操作をしている
  - Setにある要素を足す -> 別の要素を足す -> 最初に足した要素を削除する
  - 同じ値を持つ2つのクラスインスタンスを作りequalsで比較する

```java
import java.util.*;

public class TestEqualsOld {
    public static void main(String[] args) {
        Set<DistanceToNode> someSet = new HashSet<>();
        someSet.add(new DistanceToNode(1, 2));
        System.out.println("actual = " + someSet.size() + " expected = 1");
        someSet.add(new DistanceToNode(2, 3));
        System.out.println("actual = " + someSet.size() + " expected = 2");
        someSet.remove(new DistanceToNode(1, 2));
        System.out.println("actual = " + someSet.size() + " expected = 1");
        boolean flag = new DistanceToNode(3, 4).equals(new DistanceToNode(3, 4));
        System.out.println("actual = " + flag + " expected = true");
    }
}

class DistanceToNode {
    private int dist;
    private int index;

    public DistanceToNode(int dist, int index) {
        this.dist = dist;
        this.index = index;
    }

    public int getDist() {
        return dist;
    }

    public int getIndex() {
        return index;
    }
}
```
- このcodeの実行結果は以下のようになる。Setから要素を`remove`した後も要素数は2で、値が等しい2つのインスタンスをequalsで比較するとfalseが返ってくる。`equals`と`hashCode`をoverrideしていないため、superクラスであるObjectクラスの当該メソッドが代わりに使われ、値が等しいかどうかではなく、参照先が等しいかどうかを判定しているためである。

```
actual = 1 expected = 1
actual = 2 expected = 2
actual = 2 expected = 1
actual = false expected = true
```

- そこで、先ほどのコードの`equals`と`hashCode`をIntelliJを使って自動的に生成してみる。当該クラスにカーソルを置いて、右クリックして Generate > equals() and hashCode()と選択する。Templateとしてはデフォルトで選択されている"IntelliJ Default"を選んだ。すると以下のようなコードが生成された。

```java
import java.util.*;

public class TestEqualsOld {
    public static void main(String[] args) {
        Set<DistanceToNode> someSet = new HashSet<>();
        someSet.add(new DistanceToNode(1, 2));
        System.out.println("actual = " + someSet.size() + " expected = 1");
        someSet.add(new DistanceToNode(2, 3));
        System.out.println("actual = " + someSet.size() + " expected = 2");
        someSet.remove(new DistanceToNode(1, 2));
        System.out.println("actual = " + someSet.size() + " expected = 1");
        boolean flag = new DistanceToNode(3, 4).equals(new DistanceToNode(3, 4));
        System.out.println("actual = " + flag + " expected = true");
    }
}

class DistanceToNode {
    private int dist;
    private int index;

    public DistanceToNode(int dist, int index) {
        this.dist = dist;
        this.index = index;
    }

    public int getDist() {
        return dist;
    }

    public int getIndex() {
        return index;
    }

    public boolean equals(Object object) {
        if (this == object) return true;
        if (object == null || getClass() != object.getClass()) return false;
        if (!super.equals(object)) return false;
        DistanceToNode that = (DistanceToNode) object;
        return dist == that.dist &&
                index == that.index;
    }

    public int hashCode() {
        return Objects.hash(super.hashCode(), dist, index);
    }
}
```

- コードの実行結果は驚くべきことに変わらなかった! IntelliJの作ってくれた`equals`, `hashCode`のどこかに問題があるのだろうか。

```
actual = 1 expected = 1
actual = 2 expected = 2
actual = 2 expected = 1
actual = false expected = true
```

- ここで悶々と数日間頭を抱えたが、IntelliJの生成したコードをよく見ると、`equals`, `hashCode`ともに、内部でsuper.equalsやsuper.hashCodeメソッドを使っている!!! ここをsuper.equalsやsuper.hashCodeを使わないように書き換えればうまくいくのではないかと思って以下のようにコードを修正してみた。(クラス名も変更してみた。)

```java
import java.util.*;

public class TestEquals {
    public static void main(String[] args) {
        Set<DistanceToNode> someSet = new HashSet<>();
        someSet.add(new DistanceToNode(1, 2));
        System.out.println(someSet.size());
        someSet.add(new DistanceToNode(2, 3));
        System.out.println(someSet.size());
        someSet.remove(new DistanceToNode(1, 2));
        System.out.println(someSet.size());
        boolean flag = new DistanceToNode(3, 4).equals(new DistanceToNode(3, 4));
        System.out.println(flag);
    }
}

class DistanceToNode {
    private int dist;
    private int index;

    public DistanceToNode(int dist, int index) {
        this.dist = dist;
        this.index = index;
    }

    public int getDist() {
        return dist;
    }

    public int getIndex() {
        return index;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (object == null || getClass() != object.getClass()) return false;
        // if (!super.equals(object)) return false;
        DistanceToNode distanceToNode = (DistanceToNode) object;
        return dist == distanceToNode.dist &&
                index == distanceToNode.index;
    }

    @Override
    public int hashCode() {
        int result = 31;
        result = 31 * result + dist;
        result = 31 * result + index;
        return result;
        // return Objects.hash(super.hashCode(), dist, index);
    }
}
```

- コードの実行結果は以下の通り! きちんと値で比較することができるようになったようで、よかった...
 
```
actual = 1 expected = 1
actual = 2 expected = 2
actual = 1 expected = 1
actual = true expected = true
```

# あとがき
- IntelliJは便利だけど、盲信してはいけない、たまには自分で頭を使わなくてはいけない、という貴重な教訓が得られました
- なぜIntelliJでequals, hashCodeを自動生成するときに、このようなコードが作られるのだろう、何か合理的な理由があるのだろうか、と疑問に思いました
  - コメントで教えてもらったんですけど、Objectクラス以外の、何か具体的なクラスを継承しているクラスについては、親クラスのメンバー変数などが一致していないと、equalsをtrueにしたくないと思うので、そのためにこうなっているのだろうということでした。今回の場合は、Objectクラスを直接継承したクラスのため、IntelliJのこの気遣いが仇になっていると感じました。Objectクラスを直接継承したクラスかどうかをIDEが判定することは難しくないと思うから、IntelliJの開発元にコメントとかしたら修正してくれるのかな。
  - __9/14 更新__: 自分が感じた疑問と同じようなことがすでにpostされていました -> https://intellij-support.jetbrains.com/hc/en-us/community/posts/206873545-Strange-generated-hashCode-
     - Objectクラスを直接継承するクラスの場合には、親クラスのequalsやhashCodeを使わないようなコードを生成するように修正することが可能か、聞くだけ聞いてみます