1. Objects クラスの概要
本題に入りましょう。毎回 null チェックを書いたり、各フィールドのハッシュ計算を手作業で書くのに疲れているなら、ユーティリティクラス java.util.Objects が役に立ちます。目的は、オブジェクト操作をより簡潔かつ安全にすることです。
これはまさに「スイスアーミーナイフ」です。等価比較を安全に行い(NullPointerException のリスクなし)、ハッシュコードを手軽に算出し、コンパレータ経由の比較を行い、引数の null を検証できます。
Objects.equals: null を考慮した安全な比較
単に a.equals(b) と書くと、a が null の場合に 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.hash と hashCode: ハッシュを簡潔に計算
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. 例: equals、hashCode、compareTo を Objects で正しく実装する
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.equals と Objects.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 クラスの主要メソッド
| メソッド | 用途 | 使用例 |
|---|---|---|
|
null を考慮した 2 つのオブジェクトの安全な比較 | |
|
複数フィールドからハッシュコードを簡潔に算出 | |
|
コンパレータ経由の比較(null に安全) | |
|
null チェック。NullPointerException を送出 | |
|
null/非 null の判定(Stream API で便利) | |
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 が用意されています。
GO TO FULL VERSION