Code Fix

上級

equals と hashCode を必ずセットで実装する理由

HashSetやHashMapが先にハッシュ値で振り分けるためです。引数をObjectにする理由も扱います。

エラーメッセージの読み方

Main.java:8: error: cannot find symbol
Main.java
ファイル名
8
行番号 — javacが異常に気づいた位置。原因の行とは限りません
error
種別 — コンパイルエラー。実行まで到達しません
cannot find symbol
内容 — javacが期待していたもの、または見つけられなかったもの

このエラーが出る典型パターン

パターン1

 1  import java.util.Objects;
 2  
 3  public class Main {
 4      static class P {
 5          int id;
 6          P(int id) { this.id = id; }
 7          @Override public boolean equals(Object o) {
 8              return o instanceof P && ((Object) o).id == id;
                                              ^
 9          }
10          @Override public int hashCode() { return Objects.hash(id); }
11      }
12      public static void main(String[] args) {
13          System.out.println(new P(1).equals(new P(1)));
14      }
15  }
Main.java:8: error: cannot find symbol symbol: variable id location: class Object

Objectのままではフィールドにアクセスできません。instanceofで確認した型へキャストします。

直し方: Object を P にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  import java.util.HashSet;
 2  import java.util.Set;
 3  
 4  public class Main {
 5      static class P {
 6          int id;
 7          P(int id) { this.id = id; }
 8          @Override public boolean equals(Object o) { return o instanceof P && ((P) o).id == id; }
 9                                                                
                                       ^
10      }
11      public static void main(String[] args) {
12          Set<P> s = new HashSet<>();
13          s.add(new P(1)); s.add(new P(1));
14          System.out.println(s.size());
15      }
16  }
出力: 2期待: 1 (コンパイルは通っています)

equalsを上書きしたらhashCodeも必ず上書きします。HashSetは先にハッシュ値で振り分けるためです。

直し方: (空) を @Override public int hashCode() { return id; } にします。

この問題を解いてみる →

パターン3

 1  public class Main {
 2      static class P {
 3          int id;
 4          P(int id) { this.id = id; }
 5          public boolean equals(P          o) { return ((P) o).id == id; }
                                       ^
 6      }
 7      public static void main(String[] args) {
 8          Object a = new P(1);
 9          System.out.println(a.equals(new P(1)));
10      }
11  }
出力: false期待: true (コンパイルは通っています)

equalsの引数はObjectでなければオーバーライドになりません。Pにするとオーバーロードになり呼ばれません。

直し方: P を Object にします。

この問題を解いてみる →

パターン4

 1  import java.util.HashSet;
 2  import java.util.Objects;
 3  import java.util.Set;
 4  
 5  public class Main {
 6      static class P {
 7          int id;
 8          P(int id) { this.id = id; }
 9          @Override public boolean equals(Object o) { return o instanceof P && ((P) o).id == id; }
10          @Override public int hashCode() { return Objects.hash(id); }
11      }
12      public static void main(String[] args) {
13          Set<P> set = new HashSet<>();
14          P p = new P(1);
15          set.add(p);
16          p.id = 2 ;
                    ^
17          System.out.println(set.contains(p));
18      }
19  }
出力: false期待: true (コンパイルは通っています)

equals/hashCodeの計算に使うフィールドを、集合に入れたあとで書き換えるとハッシュ値の格納位置とずれ、探せなくなります。

直し方: 2 を 1 にします。

この問題を解いてみる →

パターン5

 1  import java.util.Objects;
 2  
 3  public class Main {
 4      static class Point {
 5          int x, y;
 6          Point(int x, int y) { this.x = x; this.y = y; }
 7          @Override public boolean equals(Object o) {
 8              if (!(o instanceof Point)) return false;
 9              Point p = (Point) o;
10              return x == p.x              ;
                                      ^
11          }
12          @Override public int hashCode() { return Objects.hash(x, y); }
13      }
14      public static void main(String[] args) {
15          System.out.println(new Point(1, 2).equals(new Point(1, 9)));
16      }
17  }
出力: true期待: false (コンパイルは通っています)

equalsでxしか比較していないため、yが違っても等しいと判定されてしまいます。hashCodeが使う全フィールドをequalsでも比較します。

直し方: (空) を && y == p.y にします。

この問題を解いてみる →

よくある誤解

equalsだけ実装してもListのcontainsは正しく動きます。HashSetやHashMapで初めて壊れるので、発見が遅れます。

実務での勘所

equalsとhashCodeを自分で正しく書くのは簡単ではなく、実務では長らくIDEの自動生成機能を使うのが定石でした。Java 16で正式導入されたrecordを使えば、フィールド(コンポーネント)を宣言するだけでコンパイラが整合性の取れたequals・hashCode・toStringを自動生成してくれるため、単純なデータの入れ物として使う分にはこの問題そのものを回避できます。ただし独自の等価性の定義が必要な場合は、recordでも自分で上書きする必要があります。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)