Java Tips | コレクション:重複除去

Java Java
スポンサーリンク

重複除去は「一意な世界にそろえてから考える」ためのユーティリティ

業務コードでは、同じ値が何度も出てくることがよくあります。
ユーザーIDの一覧、商品コードの一覧、タグの一覧…。

そのまま処理すると、
同じユーザーに何度もメールを送ってしまったり、
同じ商品を二重に集計してしまったりと、
「重複」がバグの原因になります。

重複除去ユーティリティは、
「ロジックを書く前に、一度“重複のない世界”にそろえる」ための小さな道具です。
これを持っていると、後続の処理がかなりシンプルになります。


基本形:List の重複を取り除く

Set に通してから List に戻す、という定番パターン

一番よくあるのは、「List から重複を取り除きたい」というケースです。
定番のやり方は、「一度 Set に入れて重複を消し、また List に戻す」です。

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public final class CollectionUtils {

    private CollectionUtils() {}

    public static <T> List<T> distinct(List<T> source) {
        if (source == null || source.isEmpty()) {
            return List.of(); // 空の不変List
        }
        Set<T> set = new LinkedHashSet<>(source); // 順序を保ったまま重複除去
        return new ArrayList<>(set);
    }
}
Java

ここでの重要ポイントは三つあります。

一つ目は、「元の List を変更していない」ことです。
ユーティリティは「入力を壊さず、新しい結果を返す」方が安全です。

二つ目は、「LinkedHashSet を使っている」ことです。
LinkedHashSet は「最初に出てきた順番」を保ったまま重複を消してくれます。
業務では「順番も意味がある」ことが多いので、
HashSet より LinkedHashSet を使うのが実務的です。

三つ目は、「戻り値は“重複がない List”だと約束されている」ことです。
distinct を通した List に対しては、
「同じ要素が二度出てこない」という前提でロジックを書けます。

具体例でイメージをつかむ

List<String> raw = List.of("A", "B", "A", "C", "B");

List<String> unique = CollectionUtils.distinct(raw);

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

この unique に対しては、
「A/B/C が一度ずつだけ出てくる」と分かっているので、
「一意な件数」「一意な一覧」を扱う処理が書きやすくなります。


Set はそもそも「重複除去済みの世界」

Set を使えるなら、それ自体が“重複除去ユーティリティ”

実は、Set 自体が「重複を許さないコレクション」です。

Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // 無視される

System.out.println(set); // [A, B] など
Java

なので、「重複を許したくない」ことが最初から分かっているなら、
「List を作ってから重複除去する」のではなく、
「最初から Set に入れていく」という設計も強力です。

Set<String> userIds = new LinkedHashSet<>(); // 順序も保ちたいなら LinkedHashSet

for (User u : users) {
    userIds.add(u.getId());
}
Java

ここでの重要ポイントは、「データ構造の選択自体が“重複除去の宣言”になる」ということです。
「この集合は一意であるべきだ」と分かっているなら、
List ではなく Set を選ぶ、という発想を持っておくと設計がきれいになります。


Map で「キーの重複」をどう扱うかを決める

「最後の値を優先する」重複除去

Map の場合、「キーが同じなら上書きされる」という性質があります。
これを利用すると、「同じキーが複数回出てきたとき、どの値を採用するか」を
重複除去のルールとして決められます。

例えば、「後から来た値を優先する」重複除去はこう書けます。

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public static <K, V> Map<K, V> distinctByKey(List<V> source, java.util.function.Function<V, K> keyExtractor) {
    if (source == null || source.isEmpty()) {
        return Map.of();
    }
    Map<K, V> result = new LinkedHashMap<>();
    for (V v : source) {
        K key = keyExtractor.apply(v);
        result.put(key, v); // 同じキーが来たら上書き
    }
    return result;
}
Java

使い方のイメージです。

record User(String id, String name) {}

List<User> users = List.of(
        new User("U1", "山田"),
        new User("U2", "佐藤"),
        new User("U1", "山田(更新後)")
);

Map<String, User> byId =
        distinctByKey(users, User::id);

System.out.println(byId.get("U1")); // User[id=U1, name=山田(更新後)]
Java

ここでの重要ポイントは、「どの値を残すかのルールを明示する」ことです。
「最初の値を残したい」のか、「最後の値を残したい」のか、
あるいは「そもそも重複があったらエラーにしたい」のか。

重複除去ユーティリティを作るときは、
このルールをメソッド名やコメントでしっかり表現しておくと、
後から読んだ人にも意図が伝わります。


Stream の distinct() とユーティリティの役割分担

Stream は「一時的な処理」、ユーティリティは「共通ルール」

Java 8 以降なら、Stream の distinct() もよく使います。

List<String> unique =
        raw.stream()
           .distinct()
           .toList();
Java

これは内部的に Set を使って重複を消してくれる便利メソッドです。

ただし、あちこちでバラバラに stream().distinct() を書いていると、
「このプロジェクトでは重複をどう扱うのか」というルールが散らばってしまいます。

「順序を保つのか」「null をどうするのか」「どのタイミングで重複を消すのか」
といった設計を一箇所にまとめたいなら、
CollectionUtils.distinct のようなユーティリティに寄せておく方が、
長期的には読みやすくなります。

ユーティリティの中で Stream を使うのはもちろんアリです。

public static <T> List<T> distinct(List<T> source) {
    if (source == null || source.isEmpty()) {
        return List.of();
    }
    return source.stream()
                 .distinct()
                 .toList();
}
Java

呼び出し側は「distinct を使う」という意識ではなく、
「“重複除去済み List” が欲しいから CollectionUtils.distinct を呼ぶ」という意識で済みます。


まとめ:重複除去ユーティリティで身につけてほしい感覚

重複除去ユーティリティは、
「ロジックを書く前に、一度“一意な世界”にそろえる」ための道具です。

List なら distinct(List) で「順序を保ったまま重複なし List」を作る。
Set を選ぶこと自体が「重複を許さない」という宣言になる。
Map では「同じキーが複数あるとき、どの値を残すか」をルールとして決める。
Stream の distinct() は便利だが、プロジェクトの共通ルールはユーティリティに寄せる。

もしあなたのコードのどこかに、

if (!list.contains(x)) {
    list.add(x);
}
Java

のような「手作業の重複チェック」が何度も出てきているなら、
それを一度「重複除去ユーティリティ」や「Set への置き換え」で整理できないか、眺めてみてください。

その小さな整理が、
「データの“一意性”を意識して、シンプルなロジックを書けるエンジニア」への、
確かな一歩になります。

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