1. はじめに
オブジェクトはどのように比較するか
Java のオブジェクトは単なるデータではなく、それぞれがメモリ上の固有のアドレスを持ちます。演算子 == は「同じ箱か?」という問い、つまり参照(アドレス)を比較し、内容は比較しません。メソッド equals は内容を比較するためのものです。デフォルトでは、equals をオーバーライドしなければ、その動作は == と同じになります。
Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);
System.out.println(p1 == p2); // false — メモリ上では別のオブジェクト!
別々のオブジェクトでもデータが等しい(たとえば意味のある全フィールドが一致する)と見なすには、equals をオーバーライドする必要があります。さらに、それらのオブジェクトをハッシュ系コレクションで使うなら、hashCode も正しくオーバーライドすることが不可欠です。
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 同一参照かどうか
if (o == null || getClass() != o.getClass()) return false; // クラスを確認
Person person = (Person) o; // 必要な型にキャスト
return age == person.age && name.equals(person.name); // フィールドを比較
}
@Override
public int hashCode() {
return Objects.hash(name, age); // HashSet/HashMap で動作するように
}
}
このようなオーバーライドは HashSet/HashMap を正しく扱ううえで極めて重要です。equals と hashCode がなければ、コレクションはデータが同じオブジェクトでも別物として扱ってしまいます。
equals における同値関係は要件次第です。全フィールドで比べても、フィールドの一部(例えば User の email のみ)で比べても構いません。重要なのは一貫性と契約の遵守です。
どこで特に重要か?
- ハッシュテーブルに基づくコレクション: HashSet、HashMap、LinkedHashSet など。
- コレクションでの検索・削除: 正しい equals がないと、目的のオブジェクトが「見つからない」ことがあります。
- ビジネスロジック: 例として、同じ email を持つ 2 つの User は同一ユーザーと見なすべき場合。
hashCode — 何のために必要か?
ハッシュ系コレクション(HashSet、HashMap など)はハッシュテーブルを使います。メソッド hashCode は整数、すなわちオブジェクトが入る「バケットのアドレス」を計算します。2 つのオブジェクトが equals で等しい場合、その hashCode は一致しなければなりません。これに反すると、コレクションの挙動は予測不能になります。
2. equals と hashCode の契約
equals の契約
equals の基本要件:
- 反射性: a.equals(a) は常に true。
- 対称性: a.equals(b) が true なら、b.equals(a) も true。
- 推移性: a.equals(b) かつ b.equals(c) なら、a.equals(c) も true。
- 一貫性: オブジェクトが変わらない限り、呼び出し結果は安定していること。
- null との比較: どんなオブジェクトも null とは等しくない。
hashCode の契約
- 2 つのオブジェクトが equals で等しいなら、その hashCode は等しい。
- オブジェクトが等しくない場合でも、ハッシュコードが一致することはあり得る(衝突は許容されるが望ましくはない)。
- オブジェクトが論理的に変化しない間は、その hashCode は一定であるべき。
言い換えると、一致する hashCode は等価の必要条件であって十分条件ではありません。同じハッシュでも equals による等価は保証されません。
3. equals と hashCode の実装: 例
Person クラスを考えます。ここでは name と age フィールドで等価を定義します。
public class Person {
private String name;
private int age;
// コンストラクタ、getter、setter...
@Override
public boolean equals(Object o) {
if (this == o) return true; // 参照の比較
if (o == null || getClass() != o.getClass()) return false; // クラスの確認
Person person = (Person) o; // 型の変換
// フィールドを比較
return age == person.age &&
(name != null ? name.equals(person.name) : person.name == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age; // 31 はよく使われる素数
return result;
}
}
- まずは高速なチェック: 参照とクラス。
- 次に意味のあるフィールドを比較。
- hashCode では衝突を減らすために素数 31 を用いるのが一般的。
Objects.equals と Objects.hash の利用
Java 7 以降、Objects クラスによりコードが簡潔になり、null にも安全になります:
import java.util.Objects;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
4. equals、hashCode、compareTo の関係
equals と compareTo はどう関係するか?
インターフェース Comparable はメソッド compareTo を定めており、負/零/正で「小さい/等しい/大きい」を表します。望ましいのは、a.compareTo(b) == 0 なら a.equals(b) が成り立つことです。逆は必ずしも必要ありません。
一貫性を崩すと(例えば compareTo は年齢だけで比較し、equals は名前と年齢で比較するなど)、TreeSet/TreeMap のようなソート済みコレクションで予期しない挙動が起こり得ます。順序の観点では「等しい」が、内容の観点では等しくないと見なされるためです。
コレクションにおける equals と hashCode
- HashSet や HashMap では、追加/検索/削除の各操作が正しい equals と hashCode の実装に依存します。
- オーバーライドしないと、データが同じでも「別物」と見なされます。
5. コレクションでの動作例
HashSet: ユニークなオブジェクトの保持
Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // 重複
System.out.println(people.size()); // 1(equals/hashCode が正しく実装されていれば)
正しい equals/hashCode がなければ、両方のオブジェクトがセットに入ってしまいます。
HashMap: キーによる検索
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);
map.put(p1, "ユーザー1");
System.out.println(map.get(p2)); // "ユーザー1"(equals/hashCode が正しく実装されていれば)
契約を満たしていないとコレクションは null を返します。コレクションにとっては「別の」キーだからです.
6. ベストプラクティス: 実装のコツ
- equals/hashCode には、オブジェクトの「同一性」を定めるすべてのフィールドを含める。
- hashCode の計算に、(コレクションに追加後に)変更される可能性のある可変フィールドを使わない。
- メソッドの自動生成は IDE に任せると、タイプミスの可能性が減る。
- equals では、まず this == o、次にクラス、その後にフィールドを確認する。
- オブジェクト型フィールドの比較には Objects.equals を使う。
- ハッシュコードには Objects.hash か、31 乗算の定番パターンを使う。
7. 役立つニュアンス
なぜ hashCode だけではだめなのか?
衝突は避けられません。異なるオブジェクトが同じ hashCode を持つことはあり得ます。ハッシュはバケットを素早く絞り込むための指標に過ぎず、最終的な等価判定は equals が行います。
equals と hashCode をオーバーライドしなくてもよいか?
オブジェクトが内容で比較されることは決してなく、キーやユニーク要素としてコレクションに使われることもないと確信できる場合のみです。実務では稀です。
==、equals、compareTo の違い
| 演算子/メソッド | 何を比較するか | 用途 |
|---|---|---|
|
参照(メモリアドレス) | 「同一オブジェクトか?」の確認 |
|
オブジェクトの内容 | ビジネスロジック上の等価 |
|
順序(小さい/等しい/大きい) | ソートや順序付け |
8. equals と hashCode 実装時の典型的なミス
誤り 1: equals はオーバーライドしたが、hashCode を忘れた。 オブジェクトは等しいと見なされても、ハッシュテーブルの別バケットに入ってしまい、検索や削除が壊れます。
誤り 2: 可変フィールドを hashCode に使っている。 コレクションに追加後にフィールドが変わると、オブジェクトは「行方不明」になります。ハッシュは変わるのに、バケットは変わらないためです。
誤り 3: equals の対称性/推移性を破っている。 a.equals(b) は true なのに b.equals(a) は false、または推移性が壊れている—コレクションの挙動が予測不能になります。
誤り 4: equals でクラスを確認していない。 異なるクラスのオブジェクトを比較すると、誤った結果や例外を引き起こします。
誤り 5: 文字列やオブジェクトを == で比較している。 演算子 == は参照を比較します。内容の比較には equals を使ってください。
誤り 6: compareTo と equals の一貫性がない。 a.compareTo(b) == 0 なのに !a.equals(b) の場合、TreeSet/TreeMap は順序上は等しいが等価ではない要素として扱い、幽霊要素や重複の原因になります。
GO TO FULL VERSION