Java | オブジェクト指向:equals のオーバーライド

Java Java
スポンサーリンク

equals をオーバーライドする意味

equals は「同じモノかどうか」を値の観点で判定するためのメソッドです。デフォルトの equals は「同じインスタンス(同一性)」しか比較しませんが、値オブジェクトやエンティティでは「中身(等価性)」で比較したい場面が多くあります。そのときに equals をオーバーライドして、何をもって“同じ”とするかをクラスの中に明確に定義します。


守るべき契約(とても重要)

equals の5つの性質

equals をオーバーライドしたら、次の性質を必ず満たす必要があります。これが破られると、コレクションやキャッシュなどで不具合が起きます。

  • 反射律(自分自身と等しい): x.equals(x) は常に true
  • 対称律(対称に等しい): x.equals(y) が true なら y.equals(x) も true
  • 推移律(連鎖して等しい): x.equals(y), y.equals(z) が true なら x.equals(z) も true
  • 一貫性(結果がぶれない): 比較対象が変わらなければ、何度呼んでも同じ結果
  • 非 null(null とは常に等しくない): x.equals(null) は常に false

加えて「equals をオーバーライドしたら hashCode も必ずオーバーライド」します。同じ値で等しいなら、同じハッシュ値でなければなりません。これが HashMap/HashSet の前提です。


基本パターンと実装テンプレート

値オブジェクト向け(推奨:同一クラスのみ等価)

「同じクラスのインスタンス同士」でだけ比較し、比較対象のフィールドを使って判定します。getClass を使うと、サブクラスとの混在比較を避けられ、対称律を守りやすいです。

import java.util.Objects;

public final class Email {
    private final String value;

    public Email(String raw) {
        var v = raw == null ? "" : raw.trim().toLowerCase(java.util.Locale.ROOT);
        if (!v.contains("@")) throw new IllegalArgumentException("invalid email");
        this.value = v;
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;                 // 同一参照
        if (o == null || getClass() != o.getClass()) return false; // 同一クラスのみ
        Email other = (Email) o;
        return Objects.equals(this.value, other.value);            // 値で比較
    }

    @Override public int hashCode() {
        return Objects.hash(value);                 // 比較に使ったフィールドでハッシュ
    }
}
Java

継承を見越す場合(instanceof を使う)

継承階層で「親型どうしも等しくしたい」なら instanceof を使います。ただし、対称律が崩れやすいので、親子で同じ比較ルールを厳密に共有できる場合に限定してください。

@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Shape s)) return false; // 親型での比較
    return Double.compare(this.area(), s.area()) == 0; // 同じ指標で比較(例)
}
Java

典型的な落とし穴(重要ポイントの深掘り)

hashCode を忘れる

equals を正しく作っても、hashCode を未定義だと HashSet/HashMap で同一扱いにならず、重複が入ったり見つからなくなります。equals に使ったフィールドと同じセットで hashCode を計算するのが鉄則です。

可変フィールドで比較する

等価に使うフィールドが後から書き換えられると、一度コレクションに入れた後にハッシュが変わり、取り出せなくなる問題が起きます。等価に使うフィールドは不変(final)にするか、格納後に変わらない設計にしてください。

継承で対称律が崩れる

親が instanceof で広く等価判定し、子が getClass で狭く比較すると、x.equals(y) は true だが y.equals(x) は false といった不整合が生まれます。継承で等価判定を共有するのは難度が高いので、値オブジェクトは原則 final クラスにして getClass 方式を推奨します。


例題で身につける

例 1: 金額と通貨の値オブジェクト(不変・安全)

import java.util.Objects;

public final class Money {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        if (amount < 0) throw new IllegalArgumentException("amount>=0");
        if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
        this.amount = amount;
        this.currency = currency.trim();
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money other = (Money) o;
        return amount == other.amount && currency.equals(other.currency);
    }

    @Override public int hashCode() {
        return Objects.hash(amount, currency);
    }
}
Java

同じ金額・同じ通貨なら等しい。不変なので、HashSet に入れても安全です。

例 2: コレクションのキーにする(equals/hashCode の効果)

var m1 = new Money(100, "JPY");
var m2 = new Money(100, "JPY");

java.util.Set<Money> set = new java.util.HashSet<>();
set.add(m1);
System.out.println(set.contains(m2)); // true(値が同じなら見つかる)
Java

equals/hashCode が正しいから、別インスタンスでも“同じ値”として扱えます。

例 3: record を使った簡潔な等価

record は equals/hashCode/toString を自動生成します。検証が必要ならコンパクトコンストラクタで定義します。

public record Point(int x, int y) {
    public Point {
        if (x < 0 || y < 0) throw new IllegalArgumentException("non-negative");
    }
}
// Point は値で等価。新規で equals/hashCode を書く必要なし
Java

実務の指針とチェックリスト

等価に使うフィールドを決める

「何をもって同じとするか」を一度決めたら、コードとテストで固定します。値オブジェクトなら“全フィールド”、エンティティなら“識別子(ID)”のみ、のように設計意図に合わせて選びます。

不変に寄せる

等価に使うフィールドが可変だとトラブルの元です。private final を基本にし、変更したい場合は新インスタンスを返すスタイルへ寄せます。

equals と hashCode をセットで

equals を作ったら、同じフィールド群で hashCode を必ずオーバーライド。Objects.hash を使うと簡潔で安全です。

テストする

反射律・対称律・推移律をテストに落とし込みます。HashSet/HashMap で contains/get が期待どおりに動くかも確認すると安心です。


仕上げのアドバイス(重要部分のまとめ)

equals のオーバーライドは「値で同じか」をクラスに刻む行為です。契約(反射・対称・推移・一貫性・非 null)を守り、hashCode を同じ基準で必ず実装する。不変に寄せてコレクションでも安全に使えるようにし、継承では安易に広げず、原則 final+getClass 比較で対称律を守る——この型を徹底すれば、等価判定のバグは激減し、コレクションやキャッシュで安心して利用できます。

タイトルとURLをコピーしました