結論: == は参照(同じオブジェクトかどうか)を比べ、equals は内容が等しいかを比べるためのメソッドです。ただし Object.equals の既定の実装は this == obj なので、自作クラスで equals をオーバーライドしないと、内容が同じでも別物と判定されます。equals をオーバーライドするときは、必ず hashCode も一緒にオーバーライドします。
== と equals の違い
| 比較 | 比べるもの | 自作クラスで上書きしない場合 |
|---|---|---|
a == b |
参照(同じインスタンスか) | 同じインスタンスのときだけ true |
a.equals(b) |
内容(クラスが定義する等価性) | Object.equals が使われ、== と同じ結果 |
オーバーライドしないと起きること
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
System.out.println(set.contains(new Point(1, 2))); // false(別のインスタンスなので)
List.contains や HashSet.contains は内部で equals を使うため、equals を上書きしていないと「同じ座標の点」を見つけられません。
正しい実装
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. 同じ参照なら等しい
if (o == null || getClass() != o.getClass()) return false; // 2. null・別クラスは等しくない
Point other = (Point) o;
return x == other.x && y == other.y; // 3. フィールドを比較
}
@Override
public int hashCode() {
return Objects.hash(x, y); // equals で使うフィールドと同じもの
}
}
- 引数の型は
Objectにします。equals(Point o)と書くとオーバーライドではなくオーバーロードになり、HashSetなどからは呼ばれません。@Overrideを付けておけば、この誤りはコンパイルエラーで分かります。 - 参照型のフィールドは
==ではなくObjects.equals(a, b)で比較します(nullも安全に扱えます)。
原理(なぜそうなるか)
Object クラスは個々のクラスの「内容」を知らないため、既定の equals は参照の一致しか判定できません。何をもって等しいとするかはクラスごとに決める必要があり、それがオーバーライドの役割です。
hashCode には「equals で等しい2つのオブジェクトは、同じ hashCode を返さなければならない」という契約があります。HashSet や HashMap は、まず hashCode で格納場所を絞り込み、その中で equals を使って比較します。equals だけを上書きして hashCode を上書きしないと、等しいはずの2つのオブジェクトが既定の hashCode(通常はインスタンスごとに異なる値)で別の場所を探されるため、contains が false になったり、同じ内容の要素が重複して入ったりします。
(この初版はAIが作成。随時書き換え)