Java / クラスとオブジェクト

equals のオーバーライドと、なぜ必要か

結論: == は参照(同じオブジェクトかどうか)を比べ、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が作成。随時書き換え)

索引へ戻る