1. equals、hashCode、toString の自動生成
これらのメソッドは何のため?
Java でオブジェクトを扱っていると、すぐに同じ種類の課題にぶつかります。ときには、2つのオブジェクトが等しいかを確認したいことがあります。例えば、Set や Map のようなコレクションに既に入っているかを知りたい場合です。別のケースでは、オブジェクトを HashMap のキーとして使いますが、その際は比較のための特別な規則が欠かせません。さらに、ログや画面にオブジェクトを出力するとき、MyClass@7b23ec81 のような意味不明な表示ではなく、意味のある形で表示したくなることがほとんどです。
そのために、Java の各クラスには次の3つの特別なメソッドがあります:
- equals(Object o) が等価性の判定を担当します。
- hashCode() はオブジェクトの数値的な「指紋」を与え、ハッシュテーブルのようなコレクションに必要です。
- toString() はデバッグや出力を大幅に楽にする、わかりやすい文字列表現を返します。
なぜ通常のクラスではつらいのか?
通常のクラスでは、これらのメソッドを手で書く必要があります。ここで退屈さと頭痛のタネが始まります。クラスを膨らませるだけの大量のボイラープレートができあがり、どこかでミスしやすいのです。フィールドの比較を忘れたり、hashCode の計算を誤って不思議なバグに悩まされたり。さらに新しいフィールドを追加したら、これらのメソッドをまた全部書き換えなければなりません。
通常のクラスの例
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
見覚えがありますか? しかもこれはたった2つのフィールドに対してだけです! もし20個あったら?
record はどうやるか
record クラスなら、これらをすべて自動で行います。次のように宣言するだけでOKです:
public record Point(int x, int y) { }
すると Java が自動生成します:
- コンストラクタ
- ゲッター(x()、y())
- equals、hashCode、toString
自動生成されるメソッド
- equals は record のすべてのコンポーネントを値で比較します。
- hashCode はすべてのコンポーネントに基づいて計算されます。
- toString は Point[x=1, y=2] のような文字列を返します。
実際に見てみましょう!
public record Point(int x, int y) {}
public class Demo {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true
System.out.println(p1); // Point[x=1, y=2]
}
}
出力:
true
true
Point[x=1, y=2]
期待どおりに動作し、余計なコードは1行もありません!
2. なぜ重要か: コレクション、デバッグ、堅牢性
コレクションでの正しい動作
例えば、オブジェクトを HashMap のキーや HashSet の要素として使うとします。もし equals と hashCode の実装が正しくないと、コレクションはおかしな振る舞いをします。さっき追加したはずの要素が見つからなかったり、逆に異なる2つのオブジェクトを同一と見なしたりします。
record クラスなら安心です。比較とハッシュは、宣言順にすべてのコンポーネントを必ず考慮します。
例: record をキーとして使う
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record Point(int x, int y) {}
Map<Point, String> map = new HashMap<>();
Point p1 = new Point(3, 4);
map.put(p1, "Hello!");
Point p2 = new Point(3, 4);
System.out.println(map.get(p2)); // "Hello!" — 動作します!
}
}
注意: p1 と p2 は別のオブジェクト(別の参照)ですが、フィールド値が同じなので等しいと見なされます。Map と HashMap についての詳細はレベル26で学びます :P
デバッグとロギングのしやすさ
通常のクラスだと Point@1a2b3c4d のような退屈な表示になりがちですが、record クラスは見やすく有益な形で出力されます:
Point[x=3, y=4]
デバッグやログ出力の時間を大幅に節約できます。
3. record 内部で equals、hashCode、toString はどう動くか
equals メソッド
record クラスの equals は、次の条件で2つのオブジェクトを等しいと判定します:
- 同じ型(同じ record クラス)である
- すべてのコンポーネントが等しい(プリミティブは ==、オブジェクトは equals())
比較の例
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
hashCode メソッド
ハッシュコードは record のすべてのコンポーネントに基づいて計算され、通常は標準の Objects.hash(...) が使われます。
System.out.println(p1.hashCode()); // 例えば、994
System.out.println(p2.hashCode()); // 同じく 994
System.out.println(p3.hashCode()); // 別の数値
toString メソッド
文字列表現は常に次の形式です:
ClassName[field1=value1, field2=value2, ...]
System.out.println(p1); // Point[x=1, y=2]
4. equals、hashCode、toString のオーバーライド: いつ・どうやって?
ときどき(多くはありませんが)、これらのメソッドの標準の振る舞いを変えたいことがあります。例えば、toString の形式を変えたい、あるいは一部のフィールドだけで比較したい、といった場合です。
注意: equals/hashCode をオーバーライドするなら、強い意図と理解を持って行ってください。両者の「契約」を破ると、追跡が難しいバグを引き起こします。
オーバーライドの方法
record クラス本体の中にメソッドを宣言するだけです:
public record Point(int x, int y) {
@Override
public String toString() {
return "(" + x + "; " + y + ")";
}
}
Point p = new Point(3, 5);
System.out.println(p); // (3; 5)
equals/hashCode をオーバーライドしてよい?
はい、ただし自信がないなら強くは勧めません。例えば、比較を x フィールドだけで行いたい(やや不自然な)場合:
public record Point(int x, int y) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x;
}
@Override
public int hashCode() {
return Integer.hashCode(x);
}
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);
System.out.println(p1.equals(p2)); // true (!)
ただし注意してください。equals をオーバーライドするなら、必ず hashCode もオーバーライドしましょう。でないとコレクションが正しく動きません。
ベストプラクティス
- 目的が明確でないなら、オーバーライドしない。
- toString は、望む形式があれば自由にカスタマイズしてよい。
- equals/hashCode は、強い理由があり影響を理解している場合に限る。
5. 実践: オブジェクトの比較とコレクションでの record の利用
例: 2つの record オブジェクトの比較
public record User(String name, int age) {}
public class Demo {
public static void main(String[] args) {
User u1 = new User("Alice", 20);
User u2 = new User("Alice", 20);
User u3 = new User("Bob", 25);
System.out.println(u1.equals(u2)); // true
System.out.println(u1.equals(u3)); // false
System.out.println(u1.hashCode() == u2.hashCode()); // true
System.out.println(u1); // User[name=Alice, age=20]
}
}
例: HashMap のキーとして record を使う
たとえば、ユーザーの名前と年齢ごとの来店回数を保存するアプリがあるとしましょう(クラブに「Ivan(20歳)」が2人いるかもしれないので)。
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record User(String name, int age) {}
Map<User, Integer> visits = new HashMap<>();
User ivan20 = new User("Ivan", 20);
User ivan22 = new User("Ivan", 22);
visits.put(ivan20, 5);
visits.put(ivan22, 2);
// 値による検索が正しく動くことを確認
System.out.println(visits.get(new User("Ivan", 20))); // 5
System.out.println(visits.get(new User("Ivan", 22))); // 2
}
}
もし equals と hashCode が正しく実装されていなければ、検索はうまくいきません。Map と HashMap の詳細はレベル26の講義で :P
6. record クラスにおける equals、hashCode、toString のよくあるミス
誤り1: 生成後にフィールドを変更できると期待する。
record のフィールドは常に final で、比較はコンストラクタで与えられた値に基づきます。もしフィールドに可変オブジェクトを持たせ、その内部状態を「こっそり」変えてしまうと、比較やハッシュが不正確になることがあります。
誤り2: equals はオーバーライドしたが、hashCode を忘れた。
これらのメソッドは片方だけでは不十分です。どちらかをオーバーライドするなら、必ずもう一方も! そうしないと HashSet や HashMap といったコレクションが予測不能な動作になります。
誤り3: toString の形式が別のものだと期待する。
特別な形式が必要なら、toString を自分でオーバーライドしてください。デフォルトの形式は常に ClassName[field1=value1, field2=value2] です。
誤り4: 可変フィールドを持つ複雑なクラスに record を使う。
record のフィールドは不変であるべきです。例えばフィールドに ArrayList を持ち、その内容が誰かに変更されると、比較やハッシュコードが「壊れる」可能性があります。record では不変な型のみを使うのが望ましいです。
誤り5: value object ではない振る舞いを持つクラスに record を使う。
Record は「短い記法の小さなクラス」ではありません。値の集合を表すための純粋な value object です。複雑なロジック、可変状態、あるいは継承が必要な場合は、通常のクラスを使いましょう。
GO TO FULL VERSION