Skip to content
Playground

3.等価性

学習目標

  • 参照の等価性と値の等価性の違いを説明できる
  • equals / hashCode / toString をオーバーライドして、値による等価判定とデバッグ表示ができる

2 つのインスタンスが等しいかを判定する方法には、==equals メソッドの 2 通りがあります。両者は判定基準が異なります。

参照と値の等価性

基本型(intdouble など)に対する == は、値が等しいかを判定します。一方、クラスのインスタンスに対する == は判定基準が異なります。

Main.java
Person p1 = new Person("田中", 25);
Person p2 = new Person("田中", 25);
System.out.println(p1 == p2); // false

フィールドの値が同じでも false になります。== はオブジェクトに対しては参照が同じかを判定するためです。new Person(...) を 2 回実行しているため、p1p2 は別々のオブジェクトを指し、参照は異なります。

Main.java
Person p1 = new Person("田中", 25);
Person p2 = p1; // 参照を代入(同じインスタンスを指す)
System.out.println(p1 == p2); // true

p2 = p1 は参照のコピーであるため、p1p2 は同じインスタンスを指します。この場合に限り ==true になります。

「参照は違っても、フィールドの値が同じなら等しい」と判定するには equals メソッドを使います。

Objectequals をそのまま使うと参照比較になるため、値比較に変えるには自作クラスで equalsオーバーライド(既存の実装を書き換え)します。

Main.java
Person p1 = new Person("田中", 25);
Person p2 = new Person("田中", 25);
System.out.println(p1.equals(p2));
false

オーバーライドしていないため、equals でも参照の比較になり、別々に new した p1p2 は等しくないと判定されます。「同じ名前・同じ年齢なら等しい」と判定するには、equalsPerson 自身でオーバーライドします。

equals をオーバーライドして、フィールドの値が一致するかで判定する形に変更します。Objects.equals を使うので、Person.java の冒頭に import java.util.Objects; を追加します。

Person.java
@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);
}

このコードは Java で equals をオーバーライドする定型パターンです。次の流れで判定します。

  1. 同じインスタンスとの比較なら true
  2. 比較相手が Person 型でなければ false
  3. nameage の値が一致するなら true、しなければ false

obj instanceof Person other は、objPerson 型かを判定し、そうであれば objPerson 型の変数 other として参照できるようにする構文です。!(...) で全体を否定しているので、「Person 型でないなら false を返す」という意味になります。instanceof の本格的な使い方は 多態性 で扱います。

Objects.equals(a, b) は、abnull でも安全に比較できるユーティリティです。name.equals(other.name) と書くと namenull のときに失敗するため、null 安全な Objects.equals を使います。

書き換えた Person で再度実行します。

Main.java
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));
true
false

equals メソッド では p1.equals(p2)false を返していましたが、オーバーライド後は nameage の値で判定されるため true になります。name だけが違う p3 との比較は false です。

hashCode は、オブジェクトを表す整数値を返すメソッドです。Java の HashMapHashSet のように、ハッシュ値でデータを高速に管理する仕組みでは「equals で等しいオブジェクトは hashCode の値も同じ」というルールが前提になっています。

hashCodeObjects.hash で簡潔に書けます。

Person.java
@Override
public int hashCode() {
return Objects.hash(name, age);
}

Objects.hash(name, age)nameage を組み合わせて整数値を計算します。equals で比較しているフィールドと同じものを渡します。

toString は、オブジェクトを文字列で表現するためのメソッドです。System.out.println にオブジェクトを渡したときや、文字列連結したときに自動で呼び出されます。

オーバーライドしないと、デフォルトでは型名とハッシュコードを並べた読みにくい文字列が返されます。

Main.java
Person p = new Person("田中", 25);
System.out.println(p);
Person@1540e19d

toString をオーバーライドして、人間が読みやすい形式に変更します。

Person.java
@Override
public String toString() {
return "Person{name=" + name + ", age=" + age + "}";
}

同じ Main.java を再度実行すると、出力が変化します。

Person{name=田中, age=25}

デバッグやログ出力で値の中身を確認するときに、定型的な情報が得られます。

カプセル化(フィールドの private 化、getter / setter)と等価性(equals / hashCode / toString)を備えた Person のコード全体は次のとおりです。

Person.java
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 の RecordLombok というライブラリで自動生成することもあります。