3.等価性
学習目標
- 参照の等価性と値の等価性の違いを説明できる
equals/hashCode/toStringをオーバーライドして、値による等価判定とデバッグ表示ができる
1. 参照と値の等価性
Section titled “1. 参照と値の等価性”2 つのインスタンスが等しいかを判定する方法には、== と equals メソッドの 2 通りがあります。両者は判定基準が異なります。
1-1. == の挙動
Section titled “1-1. == の挙動”基本型(int、double など)に対する == は、値が等しいかを判定します。一方、クラスのインスタンスに対する == は判定基準が異なります。
Person p1 = new Person("田中", 25);Person p2 = new Person("田中", 25);
System.out.println(p1 == p2); // falseフィールドの値が同じでも false になります。== はオブジェクトに対しては参照が同じかを判定するためです。new Person(...) を 2 回実行しているため、p1 と p2 は別々のオブジェクトを指し、参照は異なります。
Person p1 = new Person("田中", 25);Person p2 = p1; // 参照を代入(同じインスタンスを指す)
System.out.println(p1 == p2); // truep2 = p1 は参照のコピーであるため、p1 と p2 は同じインスタンスを指します。この場合に限り == は true になります。
1-2. equals メソッド
Section titled “1-2. equals メソッド”「参照は違っても、フィールドの値が同じなら等しい」と判定するには equals メソッドを使います。
Object の equals をそのまま使うと参照比較になるため、値比較に変えるには自作クラスで equals を オーバーライド(既存の実装を書き換え)します。
Person p1 = new Person("田中", 25);Person p2 = new Person("田中", 25);
System.out.println(p1.equals(p2));falseオーバーライドしていないため、equals でも参照の比較になり、別々に new した p1 と p2 は等しくないと判定されます。「同じ名前・同じ年齢なら等しい」と判定するには、equals を Person 自身でオーバーライドします。
2. equals のオーバーライド
Section titled “2. equals のオーバーライド”equals をオーバーライドして、フィールドの値が一致するかで判定する形に変更します。Objects.equals を使うので、Person.java の冒頭に import java.util.Objects; を追加します。
@Overridepublic boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Person other)) return false; return age == other.age && Objects.equals(name, other.name);}このコードは Java で equals をオーバーライドする定型パターンです。次の流れで判定します。
- 同じインスタンスとの比較なら
true - 比較相手が
Person型でなければfalse nameとageの値が一致するならtrue、しなければfalse
obj instanceof Person other は、obj が Person 型かを判定し、そうであれば obj を Person 型の変数 other として参照できるようにする構文です。!(...) で全体を否定しているので、「Person 型でないなら false を返す」という意味になります。instanceof の本格的な使い方は 多態性 で扱います。
Objects.equals(a, b) は、a か b が null でも安全に比較できるユーティリティです。name.equals(other.name) と書くと name が null のときに失敗するため、null 安全な Objects.equals を使います。
書き換えた Person で再度実行します。
Person p1 = new Person("田中", 25);Person p2 = new Person("田中", 25);Person p3 = new Person("鈴木", 25);
System.out.println(p1.equals(p2));System.out.println(p1.equals(p3));truefalseequals メソッド では p1.equals(p2) が false を返していましたが、オーバーライド後は name と age の値で判定されるため true になります。name だけが違う p3 との比較は false です。
3. hashCode のオーバーライド
Section titled “3. hashCode のオーバーライド”hashCode は、オブジェクトを表す整数値を返すメソッドです。Java の HashMap や HashSet のように、ハッシュ値でデータを高速に管理する仕組みでは「equals で等しいオブジェクトは hashCode の値も同じ」というルールが前提になっています。
hashCode は Objects.hash で簡潔に書けます。
@Overridepublic int hashCode() { return Objects.hash(name, age);}Objects.hash(name, age) は name と age を組み合わせて整数値を計算します。equals で比較しているフィールドと同じものを渡します。
4. toString のオーバーライド
Section titled “4. toString のオーバーライド”toString は、オブジェクトを文字列で表現するためのメソッドです。System.out.println にオブジェクトを渡したときや、文字列連結したときに自動で呼び出されます。
オーバーライドしないと、デフォルトでは型名とハッシュコードを並べた読みにくい文字列が返されます。
Person p = new Person("田中", 25);System.out.println(p);Person@1540e19dtoString をオーバーライドして、人間が読みやすい形式に変更します。
@Overridepublic String toString() { return "Person{name=" + name + ", age=" + age + "}";}同じ Main.java を再度実行すると、出力が変化します。
Person{name=田中, age=25}デバッグやログ出力で値の中身を確認するときに、定型的な情報が得られます。
5. 完成形のコード
Section titled “5. 完成形のコード”カプセル化(フィールドの private 化、getter / setter)と等価性(equals / hashCode / toString)を備えた Person のコード全体は次のとおりです。
import java.util.Objects;
public class Person { private final String name; private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) { if (age < 0) { System.out.println("不正な年齢: " + age); return; } this.age = age; }
@Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Person other)) return false; return age == other.age && Objects.equals(name, other.name); }
@Override public int hashCode() { return Objects.hash(name, age); }
@Override public String toString() { return "Person{name=" + name + ", age=" + age + "}"; }}データクラスの完成形です。Java のプログラムで頻繁に登場するパターンです。
実務では、この定型を Java の Record や Lombok というライブラリで自動生成することもあります。