重複除去は「一意な世界にそろえてから考える」ためのユーティリティ
業務コードでは、同じ値が何度も出てくることがよくあります。
ユーザー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 への置き換え」で整理できないか、眺めてみてください。
その小さな整理が、
「データの“一意性”を意識して、シンプルなロジックを書けるエンジニア」への、
確かな一歩になります。
