CodeGym /コース /JAVA 25 SELF /Objects クラス: equals、hashCode、hash のメソッド

Objects クラス: equals、hashCode、hash のメソッド

JAVA 25 SELF
レベル 29 , レッスン 1
使用可能

1. Objects クラスの概要

本題に入りましょう。毎回 null チェックを書いたり、各フィールドのハッシュ計算を手作業で書くのに疲れているなら、ユーティリティクラス java.util.Objects が役に立ちます。目的は、オブジェクト操作をより簡潔かつ安全にすることです。

これはまさに「スイスアーミーナイフ」です。等価比較を安全に行い(NullPointerException のリスクなし)、ハッシュコードを手軽に算出し、コンパレータ経由の比較を行い、引数の null を検証できます。

Objects.equals: null を考慮した安全な比較

単に a.equals(b) と書くと、anull の場合に NullPointerException が発生します。手動チェックは冗長です。Objects.equals(a, b) は次のように面倒を見てくれます。

  • 両方が null の場合 — true を返します。
  • どちらか一方だけが null の場合 — false を返します。
  • 両方とも null でない場合 — 通常の equals を呼びます。
import java.util.Objects;

String a = null;
String b = "Java";
System.out.println(Objects.equals(a, b)); // false

String c = null;
System.out.println(Objects.equals(a, c)); // true

String d = "Java";
String e = "Java";
System.out.println(Objects.equals(d, e)); // true

なぜ便利か? コードが短く、読みやすく、偶発的な NPE を防げます。

2. Objects.hashhashCode: ハッシュを簡潔に計算

equals と合わせて hashCode をオーバーライドする際、特にフィールドが多いと間違えがちです。手書きコードは冗長で壊れやすくなります。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + (name != null ? name.hashCode() : 0);
    result = 31 * result + age;
    return result;
}

メソッド Objects.hash はこの問題を解決します。短く、安全で、null もサポートします。

import java.util.Objects;

public class Person {
    private String name;
    private int age;

    // ... コンストラクタ、ゲッターなど

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

重要なポイント: Objects.hash は可変長引数(varargs)を使うため配列を生成します。ごく高負荷の箇所では手書きの hashCode の方が高速なことがあります。大半のアプリケーションでは差は僅少です。

3. Objects.compare: コンパレータへ比較を委譲

事前に用意した Comparator で 2 つのオブジェクトを比較したい場合、comparator.compare(a, b) を直接呼ぶ代わりに次を使えます。

int result = Objects.compare(a, b, comparator);

このメソッドは次のように動作します。

  • オブジェクトが等しい場合は 0 を返します。
  • null は任意の非 null オブジェクトより「小さい」と見なします。
  • それ以外は、渡されたコンパレータにロジックを委譲します。
import java.util.Comparator;
import java.util.Objects;

class Person {
    private String name;
    Person(String name) { 
        this.name = name; 
    }
    public String getName() { 
        return name; 
    }
}

public class Main {
    public static void main(String[] args) {
        Person a = new Person("Anna");
        Person b = new Person("Boris");
        Comparator<Person> byName = Comparator.comparing(Person::getName);

        System.out.println(Objects.compare(a, b, byName)); // <0 ("Anna" < "Boris" のため)
        System.out.println(Objects.compare(a, null, byName)); // >0 (a != null のため)
        System.out.println(Objects.compare(null, b, byName)); // <0 (null < b)
        System.out.println(Objects.compare(null, null, byName)); // 0
    }
}

4. Objects.requireNonNull: 「見えにくい」不具合の保険

メソッドが非 null の値のみを受け付けるべきなら、その場で検証しましょう。Objects.requireNonNull は任意のメッセージ付きで NullPointerException を送出します。

public void setName(String name) {
    this.name = Objects.requireNonNull(name, "名前は null にできません");
}

5. 例: equalshashCodecompareToObjects で正しく実装する

import java.util.Objects;

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = Objects.requireNonNull(name, "name は null にできません");
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 参照の同一性比較
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        // null を安全に考慮した比較
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // 簡潔で安全
    }

    @Override
    public int compareTo(Person other) {
        // まず名前で比較し、同じなら年齢で比較
        int cmp = name.compareTo(other.name);
        if (cmp != 0) return cmp;
        return Integer.compare(age, other.age);
    }
}

これでオブジェクトを HashSet に保存したり、HashMap のキーとして使ったり、等価比較やリストのソート(例: Collections.sort)が可能になります。

6. 実務での適用: コード削減と不具合の減少

例: アプリのユーザー一覧

適切な equals/hashCode を用意しておけば、コレクションでの検索は予測可能に動きます。

import java.util.ArrayList;
import java.util.List;

List<Person> users = new ArrayList<>();
users.add(new Person("Anna", 25));
users.add(new Person("Boris", 30));

Person search = new Person("Anna", 25);
System.out.println(users.contains(search)); // true

例: null になり得るフィールドの扱い

クラスに null になり得るフィールド(例: ミドルネーム)があるなら、Objects.equalsObjects.hash を使いましょう。

import java.util.Objects;

public class User {
    private String firstName;
    private String middleName; // null の可能性あり
    private String lastName;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(firstName, user.firstName)
            && Objects.equals(middleName, user.middleName)
            && Objects.equals(lastName, user.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, middleName, lastName);
    }
}

7. 表: Objects クラスの主要メソッド

メソッド 用途 使用例
Objects.equals(a, b)
null を考慮した 2 つのオブジェクトの安全な比較
Objects.equals(a, b)
Objects.hash(a, b, ...)
複数フィールドからハッシュコードを簡潔に算出
Objects.hash(name, age)
Objects.compare(a, b, comparator)
コンパレータ経由の比較(null に安全)
Objects.compare(p1, p2, byNameComparator)
Objects.requireNonNull(obj[, msg])
null チェック。NullPointerException を送出
Objects.requireNonNull(name, "名前は null にできません")
Objects.isNull(obj) / Objects.nonNull(obj)
null/非 null の判定(Stream API で便利)
list.stream().filter(Objects::nonNull)

8. Objects のメソッド使用時の典型的な誤り

誤り 1: Objects.equals を null になり得るフィールドで使い忘れる。 equals を直接呼んで比較すると、NullPointerException を招く可能性があります。Objects.equals(middleName, other.middleName) を使いましょう。

誤り 2: hashCode にすべてのフィールドを反映していない。 equals に参加するフィールドは hashCode にも必ず参加させてください。そうしないと HashSet/HashMap の動作が予測不能になります。

誤り 3: 手書きの hashCode に誤りがある。 係数や null チェックを正しく保つのは簡単ではありません。Objects.hash が面倒を見てくれるので、厳密な性能要件がない限りこちらを使いましょう。

誤り 4: Objects.requireNonNull を、クラスの契約上必要な箇所で使っていない。 フィールドが null を許容しないなら、コンストラクタやセッターでチェックしましょう。問題はその場で明らかになり、スタックトレースの奥で迷子になりません。

誤り 5: 配列に Objects.hash を使ってしまう。 配列には Arrays.hashCode、多次元配列には Arrays.deepHashCode を使いましょう。内容の比較にも同様に Arrays.equals/Arrays.deepEquals が用意されています。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION