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 にします。
よくある誤解
equalsだけ実装してもListのcontainsは正しく動きます。HashSetやHashMapで初めて壊れるので、発見が遅れます。
まとめ
equals と hashCode を必ずセットで実装する理由は上級でつまずきやすい項目です。上の3パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。