「安全な equals」とは何かをまず整理する
Java 初心者が最初につまずきやすいポイントのひとつが「equals の安全な使い方」です。
特にやらかしがちなのが、null かもしれない変数に対して、いきなり xxx.equals(...) を呼んでしまうパターンです。
String name = null;
if (name.equals("Taro")) { // ここで NullPointerException
System.out.println("Taro です");
}
Javaこのように、「左側が null かもしれない equals 呼び出し」は、業務コードでは事故の元です。
「安全な equals」とは、null が混ざっても NullPointerException を起こさず、意図した比較結果を返してくれる書き方やユーティリティのことだと考えてください。
ここから、「なぜ危ないのか」「どう書けば安全なのか」を、順番にかみ砕いていきます。
equals と == の違いをちゃんと理解する
参照比較と内容比較の違い
まず大前提として、== と equals は意味が違います。
== は「同じオブジェクトかどうか」(参照比較)を見ます。equals は「中身が同じかどうか」(内容比較)を見ます(クラスが適切に equals をオーバーライドしている前提)。
String a = new String("abc");
String b = new String("abc");
System.out.println(a == b); // false(別オブジェクト)
System.out.println(a.equals(b)); // true(中身は同じ)
Java業務で「コード値が同じか」「ID が同じか」を判定したいときは、基本的に equals を使うべきです。== を使うのは、「同じインスタンスかどうか」を見たい特殊な場面に限られます。
equals が呼ばれる側と呼ぶ側
a.equals(b) を呼ぶとき、「a がレシーバ(呼ばれる側)」「b が引数(比較対象)」です。
ここで重要なのは、「レシーバが null だと、その瞬間に NPE」という事実です。
String a = null;
String b = "abc";
a.equals(b); // ここで即 NPE
Java一方、b.equals(a) なら、b が null でない限り NPE にはなりません。
この「どっち側から equals を呼ぶか」が、安全な equals の第一のポイントになります。
典型パターン:「リテラル側から equals」を徹底する
String 比較の安全な書き方
業務で一番よく出てくるのは、文字列の比較です。
ここでの鉄板パターンが「リテラル(定数)側から equals を呼ぶ」書き方です。
String status = getStatus(); // null かもしれない
if ("ACTIVE".equals(status)) {
System.out.println("有効です");
}
Javaこの書き方だと、status が null でも NPE にはなりません。
なぜなら "ACTIVE" は絶対に null にならないからです。
逆に、次のように書くと危険です。
if (status.equals("ACTIVE")) { // status が null だと NPE
...
}
Javaこの「リテラル側から equals」は、現場でもほぼ常識レベルで使われているテクニックです。
特に String 比較では、反射的にこの形で書けるようになると、NPE のリスクが一気に減ります。
変数どうしの比較での工夫
両方とも変数で、どちらも null かもしれない場合は、「どちらか一方は必ず非 null にする」ことができません。
この場合は、後述する Objects.equals を使うのが素直で安全です。
Objects.equals を使った null セーフな比較
Objects.equals の挙動を理解する
Java 7 以降には、java.util.Objects.equals(a, b) というユーティリティメソッドがあります。
これは「両方が null なら true」「片方だけ null なら false」「両方非 null なら a.equals(b)」という挙動をします。
import java.util.Objects;
String a = null;
String b = "abc";
String c = "abc";
System.out.println(Objects.equals(a, b)); // false
System.out.println(Objects.equals(b, c)); // true
System.out.println(Objects.equals(null, null)); // true
Javaここでの重要ポイントは、「どちらか、あるいは両方が null でも NPE にならない」ということです。
つまり、「null かもしれない 2 つの値を比較したい」ときの、最も安全で素直な手段が Objects.equals です。
実務での使いどころ
例えば、DTO とエンティティの差分チェックなど、「両方とも外部から来た値で、null かもしれない」場面では、Objects.equals が非常に役立ちます。
if (!Objects.equals(oldUser.getName(), newUser.getName())) {
System.out.println("名前が変更されました");
}
Javaこのように書いておけば、どちらかの getName() が null でも安全に比較できます。null を意識した if 文を毎回書くより、意図が明確で読みやすいコードになります。
equals 実装側での「安全さ」も意識する
自作クラスの equals 実装の基本形
業務では、自分で定義したクラスに equals を実装することもあります。
このときも、「安全な equals」を意識しておくと、後から使う側が楽になります。
典型的な実装パターンは次のような形です。
public class User {
private final String id;
private final String name;
// コンストラクタなどは省略
@Override
public boolean equals(Object o) {
if (this == o) return true; // 同一インスタンスなら true
if (o == null || getClass() != o.getClass()) return false; // null やクラス違いは false
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
Javaここでも Objects.equals を使うことで、フィールドが null でも安全に比較できます。equals の中でさらに NPE を起こすような実装は、業務ではかなり危険です。
equals と hashCode のセットでの安全性
equals をオーバーライドしたら、hashCode も必ずセットでオーバーライドする、というのは有名なルールです。HashMap や HashSet に入れたときの挙動が壊れるからです。
ここでのポイントは、「equals で使っているフィールドと、hashCode で使っているフィールドを一致させる」ことです。Objects.hash(...) を使えば、null を含んだフィールドでも安全にハッシュ値を計算できます。
実務でよくある「安全な equals ユーティリティ」の例
null と空文字を同一視したい場合
業務要件によっては、「null と空文字は同じものとして扱いたい」ということがあります。
その場合、標準の equals ではなく、プロジェクト独自の「安全な equals」を用意することがあります。
public final class SafeEquals {
private SafeEquals() {}
// null と空文字を同一視する equals
public static boolean equalsNullOrEmpty(String a, String b) {
String aNorm = (a == null || a.isEmpty()) ? null : a;
String bNorm = (b == null || b.isEmpty()) ? null : b;
return Objects.equals(aNorm, bNorm);
}
}
Java使い方はこうです。
String a = null;
String b = "";
System.out.println(SafeEquals.equalsNullOrEmpty(a, b)); // true とみなす
Javaこのように、「何を同じとみなすか」をユーティリティに閉じ込めておくと、
業務ロジック側はそのポリシーに乗っかるだけで済みます。
大文字小文字を無視した比較
文字列の比較で「大文字小文字を区別しない」要件もよくあります。String には equalsIgnoreCase がありますが、これもレシーバが null だと NPE です。
public static boolean equalsIgnoreCaseSafe(String a, String b) {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
return a.equalsIgnoreCase(b);
}
Javaこのように、「標準メソッドをそのまま呼ぶと NPE の可能性がある」ものは、
一段ラップして「安全版」を用意しておくと、現場の安心感がかなり違います。
equals を使うときに常に意識してほしいこと
「この変数は null になり得るか?」を自問する
equals を書くとき、まず自分に問いかけてほしいのはこれです。
「今比較しようとしている変数は、null になり得るか?」
なり得るなら、次のどれかを選ぶべきです。
"定数".equals(変数) の形にする。Objects.equals(a, b) を使う。
自前の null セーフなユーティリティを使う。
逆に、「ここは絶対に null ではない」と言い切れるなら、その前提をコードで保証する(コンストラクタでチェックする、Objects.requireNonNull を使うなど)ことも大事です。
「何を同じとみなすか」を設計として決める
equals は単なるメソッド呼び出しではなく、「同一性の定義」です。
業務的に「同じ」とみなす条件を、きちんと設計として決める必要があります。
ID が同じなら同じユーザーとみなすのか。
名前と生年月日が同じなら同じとみなすのか。null と空文字を同じとみなすのか、別物とみなすのか。
これを曖昧にしたまま equals を使うと、「画面 A と画面 B で判定が違う」といった不具合につながります。
プロジェクト共通の「安全な equals」のルールを決めて、それをユーティリティやドメインクラスに落とし込むのが、実務ではとても重要です。
まとめ:初心者が「安全な equals」で身につけるべき感覚
安全な equals を身につけるうえで、外したくないポイントを整理します。
equals と == の違いを理解し、「内容比較には equals」を徹底する。null かもしれない変数に対しては、レシーバにしない(リテラル側から equals、Objects.equals を使う)。
自作クラスの equals では、Objects.equals を使ってフィールドが null でも安全に比較できるようにする。
「何を同じとみなすか」(null と空文字、大文字小文字など)をチームで決め、それをユーティリティに閉じ込める。
ここまでの感覚が体に入ると、「equals を書くときに毎回ビクビクする」状態から抜け出せます。
