Java

Setで重複削除できない原因をJavaで解説

AIによる要約

JavaのSetは重複を持たないコレクションですが、自作クラスを入れたときに期待通り重複削除されないことがあります。原因の多くは、equalshashCodeの実装、または何を重複とみなすかの設計です。

新人SE
新人SE
Setに入れれば重複は消えると思っていたのに、同じデータが残ります。なぜですか?
ポンコツSE
ポンコツSE
Setは魔法の重複削除ではありません。何を同じとみなすかは、入れるオブジェクトのequalsとhashCodeで決まります。

Setは重複を許さないコレクションです。文字列や数値なら、直感通りに重複が消えることが多いです。

しかし、自分で作ったクラスをHashSetに入れると、同じ内容に見えるのに重複が残ることがあります。

この記事のポイント

  • Setの重複判定はequalshashCodeに依存する
  • 自作クラスでは同一判定を実装しないと重複削除できないことがある
  • 何を重複とみなすかは業務ルールで決める
  • Streamのdistinctでも同じ観点が必要

Stringなら重複削除できる

まず、Stringのようにequals/hashCodeが適切に実装されている型では、Setで重複削除できます。

Set<String> codes = new HashSet<>();
codes.add("A001");
codes.add("A001");
codes.add("B001");

System.out.println(codes.size()); // 2

この例では、A001が2回追加されていますが、Setのサイズは2になります。

自作クラスではそのままだと重複する

次に、商品コードを表す自作クラスを作った場合を考えます。

public class ProductCode {
    private final String value;

    public ProductCode(String value) {
        this.value = value;
    }
}
Set<ProductCode> codes = new HashSet<>();
codes.add(new ProductCode("A001"));
codes.add(new ProductCode("A001"));

System.out.println(codes.size()); // 2になる可能性がある

同じA001に見えますが、ProductCodeequals/hashCodeがなければ、別々のオブジェクトとして扱われます。

ProductCodeをSetに入れたとき、equalsとhashCodeがない場合はsize 2、ある場合はsize 1になることを比較する図解。
Setで重複しないかは、equalsとhashCodeの実装で変わります。

equalsとhashCodeを実装する

商品コードの値が同じなら同じ商品コードとして扱いたいなら、そのルールをequalshashCodeで表します。

public class ProductCode {
    private final String value;

    public ProductCode(String value) {
        this.value = Objects.requireNonNull(value);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof ProductCode other)) {
            return false;
        }
        return Objects.equals(this.value, other.value);
    }

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

これで、new ProductCode("A001")同士を同じものとして扱えるようになります。

Streamのdistinctでも同じ

Setではなく、Streamdistinctを使う場合も考え方は同じです。

List<ProductCode> uniqueCodes = codes.stream()
        .distinct()
        .toList();

distinctも、要素の同一判定にequalsを使います。自作クラスの同一判定が決まっていなければ、期待通りに重複は消えません。

業務上の重複とJava上の重複は違う

現場で大事なのは、Javaの仕組みだけではありません。業務上、何を重複とみなすかを決める必要があります。

public class Customer {
    private String customerNo;
    private String name;
    private String phoneNumber;
}

このクラスで、顧客番号が同じなら重複なのか、電話番号が同じなら重複なのか、名前まで同じなら重複なのかで実装は変わります。

重複判定を決める質問

  • 業務上の一意キーは何か
  • 画面に同じように見えるだけで、本当に同じデータか
  • DBのユニーク制約と同じ基準か
  • 大文字小文字や前後空白をどう扱うか
  • 削除済みデータや履歴データを同じ扱いにしてよいか

現場でよくある失敗

CSV取込や外部API連携では、重複排除がよく出てきます。新人がSetに入れれば解決すると考えて実装すると、レビューで止まることがあります。

// レビューコメント例
// Setで重複排除していますが、ImportUserにequals/hashCodeがありません。
// 今回はメールアドレスが同じなら重複とする仕様なので、
// emailを基準にするか、emailだけをSetに入れる形にしてください。

ここで大事なのは、Setを使うかどうかではなく、重複判定の基準が仕様と一致しているかです。

単純なキーだけSetに入れる方法もある

自作クラスのequals/hashCodeを実装するより、単純なキーだけをSetに入れる方が分かりやすい場合もあります。

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

for (ImportUser user : users) {
    if (!emails.add(user.getEmail())) {
        errors.add("メールアドレスが重複しています: " + user.getEmail());
    }
}

この書き方なら、メールアドレスの重複を見ていることが明確です。新人のうちは、読み手がすぐに理解できる実装を選ぶのも大事です。

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

Set重複削除の確認リスト

  • 何を重複とみなすかを仕様で確認したか
  • 自作クラスならequals/hashCodeがあるか
  • equalshashCodeの比較フィールドが揃っているか
  • 可変フィールドを重複判定に使っていないか
  • 単純なキーだけをSetに入れる方が読みやすくないか

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

まとめ

Setは便利ですが、何を同じとみなすかを自動で判断してくれるわけではありません。自作クラスを使う場合はequals/hashCodeを確認し、業務上の重複基準と実装が一致しているかをレビュー前に見直しましょう。

-Java
-, , , ,