Java Tips | 基本ユーティリティ:安全なequals

Java Java
スポンサーリンク

「安全な 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) なら、bnull でない限り NPE にはなりません。
この「どっち側から equals を呼ぶか」が、安全な equals の第一のポイントになります。


典型パターン:「リテラル側から equals」を徹底する

String 比較の安全な書き方

業務で一番よく出てくるのは、文字列の比較です。
ここでの鉄板パターンが「リテラル(定数)側から equals を呼ぶ」書き方です。

String status = getStatus();  // null かもしれない

if ("ACTIVE".equals(status)) {
    System.out.println("有効です");
}
Java

この書き方だと、statusnull でも 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 も必ずセットでオーバーライドする、というのは有名なルールです。
HashMapHashSet に入れたときの挙動が壊れるからです。

ここでのポイントは、「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 を書くときに毎回ビクビクする」状態から抜け出せます。

タイトルとURLをコピーしました