HashSet.addがfalseを返す理由と戻り値で重複判定する方法を解説する記事のアイキャッチ。

Java クイズ

HashSet.addがfalseを返す理由|戻り値で重複を判定する方法

AIによる要約

HashSet.addは、要素が新しく追加されSetの内容が変わった場合にtrue、同じ要素がすでに存在して追加されなかった場合にfalseを返します。重複を検知するだけならcontainsしてからaddする二段階処理ではなく、addの戻り値を使えます。ただし、自作クラスの重複判定はequalsとhashCodeに依存します。入力順を保つならLinkedHashSetも検討します。

この記事はXへ投稿したJavaクイズの詳しい解説です。まず問題を解きたい方は、Xの元ポストを開くことができます。

新人SE
新人SE
Setへaddしただけなのに、戻り値のfalseはエラーという意味ですか?
ポンコツSE
ポンコツSE
エラーではありません。同じ要素がすでにあり、Setの内容が変わらなかったという意味です。

Setは重複を持たないコレクションとして学びますが、addの戻り値まで使う機会は少ないかもしれません。CSV取込や一括登録では、このbooleanが重複行を見つける簡潔な手段になります。

ただし、文字列では期待通りでも自作DTOでは重複を検知できないことがあります。Setが何を同じ要素とみなすかは、equalsとhashCodeで決まるためです。

この記事のポイント

  • 新規追加ならtrue、既存要素ならfalseを返す
  • containsしてからaddする必要はない
  • 自作クラスの重複判定はequalsとhashCodeに依存する
  • 入力順が必要ならLinkedHashSetを検討する
  • 重複を無視するかエラーにするかは業務仕様で決める

まずはXのJavaクイズを確認する

空のHashSetへ文字列Aを2回addした戻り値を問うJavaクイズ画像。
同じAを2回追加すると何が出るでしょうか。
Set<String> names = new HashSet<>();
System.out.println(names.add("A"));
System.out.println(names.add("A"));

答えはtrue、falseです。最初のAは追加され、2回目はすでに存在するため追加されません。

HashSet.addは最初true、重複した2回目falseを返すと説明する画像。
答えはtrue、falseです。2回目はSetの内容が変わりません。

元の投稿は、Xの出題ポスト回答ポストでも確認できます。

addのbooleanはSetが変化したかを表す

Collectionのaddは、呼び出しによってコレクションの内容が変わった場合にtrueを返します。Setでは同じ要素を追加できないため、既存要素ならfalseです。例外ではなく正常な結果です。

Set<String> codes = new HashSet<>();

boolean first = codes.add("A01");  // true
boolean second = codes.add("A01"); // false

System.out.println(codes); // [A01]

戻り値を使わずaddすることもできますが、重複を業務上検知したいなら捨てずに分岐へ使います。

containsしてからaddしなくてよい

重複確認のためにcontainsを呼び、その後addするコードをよく見ます。しかしadd自体が重複確認を行い、結果を返します。

// 冗長な例
if (codes.contains(code)) {
    duplicateCodes.add(code);
} else {
    codes.add(code);
}

// 戻り値を使う例
if (!codes.add(code)) {
    duplicateCodes.add(code);
}

短くすることだけが目的ではありません。『追加を試し、追加できなかったら重複として記録する』という一連の意図が1か所にまとまります。

自作クラスではequalsとhashCodeが必要

HashSetはhashCodeで候補位置を探し、equalsで同じ要素か確認します。両方を適切に実装していないDTOを追加すると、同じ社員番号でも別要素として残ることがあります。

record EmployeeKey(String companyCode, String employeeNo) {}

Set<EmployeeKey> keys = new HashSet<>();
System.out.println(keys.add(new EmployeeKey("C01", "E001"))); // true
System.out.println(keys.add(new EmployeeKey("C01", "E001"))); // false

recordは構成要素に基づくequalsとhashCodeを生成します。通常クラスで実装する場合は、何を同一性に含めるか決め、可変項目を安易に使わないでください。

CSV取込で重複行を検知する

一括取込では、DBへ問い合わせる前にファイル内の重複を見つけられます。行番号も合わせて保持すると、利用者へ修正箇所を返しやすくなります。

Set<String> seenOrderNos = new HashSet<>();
List<Integer> duplicateRows = new ArrayList<>();

for (int i = 0; i < rows.size(); i++) {
    String orderNo = rows.get(i).orderNo();
    if (!seenOrderNos.add(orderNo)) {
        duplicateRows.add(i + 2); // ヘッダー分を加味
    }
}

Setで重複を消して正常処理を続けるのか、重複行をエラーとして全件中止するのかは仕様次第です。黙って消すと、利用者が投入した件数と登録件数が合わず問い合わせになります。

順序が必要ならLinkedHashSet

HashSetの走査順を入力順として利用してはいけません。重複を除きつつ最初に出現した順序を維持したいならLinkedHashSetを使います。

Set<String> unique = new LinkedHashSet<>(
        List.of("B", "A", "B", "C"));

System.out.println(unique); // [B, A, C]

並び順がコード順や名前順という仕様なら、LinkedHashSetではなくソートが必要です。『たまたま同じ順』をテストで期待値にしないようにします。

nullと可変オブジェクトの注意

HashSetはnullを1つ保持できますが、nullを有効な要素として扱うかは別問題です。また、追加後にequals・hashCodeへ使う項目を書き換えると、containsやremoveで見つけられなくなることがあります。

Set<User> users = new HashSet<>();
User user = new User("U001");
users.add(user);

user.setId("U999"); // hashCodeに使う値を変更
System.out.println(users.contains(user)); // 期待どおりにならない可能性

Setの要素として使う識別値は不変にするか、変更前にremoveして変更後にaddし直します。可能なら識別子専用の不変クラスを使います。

JUnitで戻り値と最終状態を確認する

addのbooleanだけでなく、最終的な要素数と内容を確認します。独自クラスでは同一・非同一になる境界もテストします。

@Test
void 同じ社員番号は2回追加できない() {
    Set<EmployeeKey> keys = new HashSet<>();

    assertTrue(keys.add(new EmployeeKey("C01", "E001")));
    assertFalse(keys.add(new EmployeeKey("C01", "E001")));
    assertEquals(1, keys.size());
}

現場レビューでよくある指摘

Setのレビューでは重複が消えたかだけでなく、何を同じとみなし、重複を利用者へどう返すかを確認します。

// レビューコメント例
containsの後にaddしていますが、addの戻り値で重複を判定できます。
判定と追加を1か所へまとめてください。

// レビューコメント例
EmployeeにequalsとhashCodeがないため、同じ社員番号でも重複判定できません。
業務上の同一性を決めて両方を実装してください。

// レビューコメント例
HashSetの走査順へ依存しています。入力順を残す仕様ならLinkedHashSet、
コード順なら明示的なソートを使ってください。

Setへ変えれば重複問題が自動的に解決するわけではありません。同一性、順序、重複時の扱いを仕様として説明し、テストへ固定します。

提出前のセルフチェック

レビュー前に確認すること

  • addの戻り値を重複判定に使えるか
  • containsとaddを重ねていないか
  • equalsとhashCodeが同じ項目を使うか
  • Set追加後に識別項目を変更しないか
  • 重複を無視・警告・エラーのどれにするか
  • 順序要件に合うSetを選んだか
  • null要素を許可する必要があるか
  • 戻り値と最終要素数をテストしたか

コレクションとequalsを実践的に学ぶ参考書

Setの挙動はequals、hashCode、可変性とつながります。Javaの基本コレクションを整理した上で、値オブジェクトの設計まで学ぶと実務での判断が安定します。

書籍「スッキリわかるJava入門 実践編 第5版」の表紙
商品画像:Amazon.co.jp
PR Java基礎学習後の若手向け

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

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

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

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

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

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

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

まとめ

HashSet.addは新しい要素が追加されたときtrue、すでに同じ要素があり内容が変わらないときfalseを返します。重複検知にはcontainsとaddを重ねず、この戻り値を使えます。

自作クラスではequalsとhashCode、入力順が必要ならLinkedHashSet、重複時に無視するかエラーにするかという業務仕様まで確認してください。

-Java, クイズ
-, , , ,