JavaのList.containsがfalseになる原因を解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアがListとequalsの関係を学ぶイラスト。

Java

JavaのList.containsがfalseになる原因|equalsを実装しても見つからない時の確認点

AIによる要約

List.containsは、見た目やフィールド値を自動で比べるのではなく、要素との同値判定で一致を探します。自作クラスでequalsを実装していない、比較項目が違う、型や文字列が揃っていない場合は、値があるように見えてもfalseになります。デバッガで全項目を見る前に、何を同じとみなすクラスなのかを確認することが近道です。

新人SE
新人SE
デバッガでは同じIDに見えるのに、List.containsがfalseになります。Listの不具合でしょうか?
ポンコツSE
ポンコツSE
Listより先に、要素クラスのequalsを確認しましょう。containsは「IDが同じなら一致」と推測してくれません。クラス側が定義した同値性で判断します。

業務システムでは、取得済みのユーザー一覧に対象ユーザーが含まれるか、選択済み明細に同じ商品があるか、といった確認でList.containsを使います。StringやIntegerでは期待通り動くのに、自作クラスへ変えた途端にfalseになることがあります。

この問題は、Listの使い方だけを見ても解決しません。equals、比較対象の型、文字列の正規化、EntityとDTOの混在まで順番に確認する必要があります。

この記事のポイント

  • containsは要素の同値判定で一致を探す
  • 自作クラスはequalsがなければ別インスタンスを同じとみなせない
  • 比較項目、型、null、空白、大文字小文字も確認する
  • equalsを直す時はhashCodeもセットで見直す

List.containsは何を比較しているのか

contains(target)は、List内にtargetと同じと判断できる要素があればtrueを返します。JavaのCollection契約では、概ねObjects.equals(target, element)に相当する同値判定で考えられます。

List<String> names = List.of("Sato", "Suzuki");

System.out.println(names.contains("Sato"));   // true
System.out.println(names.contains("sato"));   // false

Stringは文字列内容でequalsが実装されています。ただし大文字小文字は区別されます。containsが曖昧検索や部分一致をしているわけではありません。

自作クラスでfalseになる基本例

次のUserクラスにはequalsがありません。IDと名前が同じでも、別々に生成したインスタンスは同じとは判定されません。

public class User {
    private final Long id;
    private final String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

List<User> users = List.of(new User(10L, "Sato"));
boolean exists = users.contains(new User(10L, "Sato")); // false

Objectの既定のequalsは、実質的に同じインスタンスかどうかを基準にします。デバッガ上でフィールドが同じでも、別々にnewしたオブジェクトなら一致しません。

equalsで何を同じとするか決める

UserをIDで識別する設計なら、IDを同値判定に使う例は次のようになります。

@Override
public boolean equals(Object other) {
    if (this == other) {
        return true;
    }
    if (!(other instanceof User user)) {
        return false;
    }
    return Objects.equals(id, user.id);
}

@Override
public int hashCode() {
    return Objects.hash(id);
}

これで同じIDのUserを一致とみなせます。ただし、IDがnullの新規Entityを複数作る可能性がある場合、null同士を同一とみなしてよいかは別途検討が必要です。JPA Entityのequalsは永続化前後でIDが変わるため、単純なテンプレート適用が危険なこともあります。

equalsはcontainsを通すためのテクニックではなく、そのクラスで「同じもの」とみなす業務上の定義です。

equalsを実装したのにfalseになる原因

比較項目が想定と違う

IDだけを見たいのに、IDE生成のequalsが名前や更新日時まで比較していると、一部の値が違うだけでfalseになります。

return Objects.equals(id, user.id)
        && Objects.equals(name, user.name)
        && Objects.equals(updatedAt, user.updatedAt);

比較項目を減らす前に、クラスがEntity、値オブジェクト、画面DTOのどれなのかを確認します。都合よくcontainsをtrueにするのではなく、同値性の意味を決めます。

EntityとDTOを混ぜている

List<UserEntity>UserDtoを渡しても、型が異なれば通常は一致しません。両方に同じIDがあっても、containsはIDだけを自動抽出しません。

List<UserEntity> users = userRepository.findAll();
UserDto target = new UserDto(10L, "Sato");

boolean exists = users.contains(target); // false

IDの存在確認が目的なら、比較したい値を明示します。

boolean exists = users.stream()
        .anyMatch(user -> Objects.equals(user.getId(), target.id()));

文字列に空白や大文字小文字の差がある

コード値をStringで比較している場合、"A01""a01""A01""A01 "は異なります。DBのCHAR型、CSV、外部APIから入った値では末尾空白も確認します。

ただし、containsの直前で毎回trimするより、入力境界で正規化する方が安全です。どの層で正規化するかをプロジェクト内で統一します。

containsを使うべきかanyMatchを使うべきか

オブジェクト全体の同値性で探すならcontainsが簡潔です。特定の項目だけを条件にしたいなら、anyMatchの方が目的を読み取りやすくなります。

確認したいこと向く方法
同じUserオブジェクトが含まれるかcontains
同じIDのUserが含まれるかanyMatch
同じコードを持つ要素があるかanyMatch
大量データを何度も検索するSetやMapへの変換を検討

「containsがfalseだからequalsを変更する」前に、そもそもオブジェクト全体の同値比較が要件なのかを見直してください。

現場での調査手順

  1. Listの要素型と、containsへ渡した型を確認する
  2. 対象クラスのequals実装の有無を確認する
  3. equalsが比較しているフィールドを確認する
  4. null、空白、大文字小文字、IDの型を確認する
  5. 特定項目の検索ならanyMatchへ変える
  6. SetやMapでも使うならhashCodeとの整合性を確認する

デバッガでは、Listの中身とtargetを眺めるだけでなく、target.equals(element)の結果を評価すると原因へ近づきやすくなります。

現場レビューでの見られ方

// レビューコメント例
containsはUser全体のequals判定になります。
要件は同一IDの存在確認なので、IDを明示したanyMatchの方が意図を読めます。

// レビューコメント例
画面都合でEntityのequalsを変更すると他のコレクションへ影響します。
DTO側で比較するか、検索条件を明示してください。

// レビューコメント例
equalsを変更する場合はhashCodeも同じ項目で生成されているか確認してください。

レビュー前のセルフチェック

確認リスト

  • List要素と検索対象の型が一致しているか
  • クラスの同値性が業務上の意味と合っているか
  • equalsの比較項目が多すぎたり少なすぎたりしないか
  • 特定項目の検索をcontainsで無理に表現していないか
  • equalshashCodeが同じ項目に基づいているか
書籍「スッキリわかるJava入門 実践編 第5版」の表紙
商品画像:Amazon.co.jp
PR Java基礎学習後の若手向け

スッキリわかるJava入門 実践編 第5版

中山 清喬 (著), 株式会社フレアリンク (監修)

基礎文法の次に必要な、現場寄りのJava知識を補う。

コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。

  • Java基礎の次に何を学ぶか迷っている
  • コレクションやStreamを整理したい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

List.containsがfalseになる時は、Listそのものより、要素クラスの同値判定を確認します。自作クラスにequalsがない、比較項目が想定と違う、EntityとDTOを混ぜている、文字列が揃っていないといった原因が代表的です。

特定のIDやコードを探したいだけなら、無理にequalsを変更せず、anyMatchで条件を明示する方法もあります。「何を同じとみなすのか」を先に決めることが、現場で安全に直すための基準です。

-Java
-, , , ,