Javaで重複チェックを行うなら、単純な排除は「Set(HashSet)」、順序保持が必要なら「LinkedHashSet」、出現回数を数えたいなら「Map(HashMap)」、関数型スタイルで書きたいなら「Stream APIのdistinct()」が最適解です。
List・Set・Stream・Mapを使った4つの重複チェックパターンを、実行可能なサンプルコード付きでまとめました。
「Listのcontainsで重複チェックしたい」「複数キーで重複判定したい」といった個別ケースにも対応できるよう、データ量や要件に応じた最適な選び方まで、現役Javaエンジニア10年以上の経験にもとづいています。
Javaの重複チェックとは?

重複チェックとは、データの中に「同じものが混ざっていないか」を確認して、見つかったら取り除いたりエラーとして扱ったりする作業のことです。
重複したデータが入り込むとシステムの動きが変になったり、集計結果がズレたりするため、重複チェックはデータの信頼性を保つために欠かせない工程です。
重複チェックが必要になるよくあるケース
開発現場で重複チェックが求められる場面は多岐にわたります。
- ユーザー登録時のメールアドレス確認:すでに登録されているメールアドレスで、別の人がアカウントを作れないように制限をかける
- CSVファイルの取り込み:外部システムから連携されたデータに、同じIDの商品が含まれていないか検証
- キャンペーンの応募:受付1人のユーザーが何度も応募できないよう、IDで制限をかける
メールアドレス確認・CSV重複検証・応募制限といった処理を適切に実装しないと、データ不整合という重大なバグにつながります。
Javaで重複チェックするときの基本的な考え方
重複を判定するための基準は「何をもって同じとみなすか」です。
プリミティブ型(intやdoubleなど)であれば数値が同じかどうかで判断しますが、オブジェクトの場合は比較のルールを自分で定義する必要があります。
IDが同じなら同じとみなすのか、すべてのフィールドが一致して初めて重複とみなすのか、設計段階で決める必要があります。
Javaでは、この判定にequalsメソッドを利用します。
Javaで重複チェックする4つの方法

Javaには重複をチェックするための手段がいくつか用意されています。
もっとも効率的で推奨されるのは「Set」を使う方法です。
理由は、Setというデータ構造自体が「重複を許さない」という性質を持っているため、追加するだけで自動的に重複排除ができるからです。
しかし、要件によってはListやStream APIを使うべき場面もあります。
ここでは代表的な4つの手法を紹介します。
| 方法 | 用途 | 速度 | 順序保持 | 重複カウント |
|---|---|---|---|---|
| HashSet | 単純な重複排除 | ◎ | × | × |
| LinkedHashSet | 順序を保った重複排除 | ○ | ○ | × |
| Stream.distinct() | 関数型スタイルで重複排除 | ○ | ○ | × |
| HashMap | 重複の出現回数を集計 | ◎ | × | ○ |
それぞれの特徴を理解したうえで、データ量・順序保持の要否・集計の必要性に応じて使い分けます。
Listで重複を排除・チェックする方法|contains・frequency比較
Listを使った重複チェックは、もっとも原始的ですが、基本原理を理解するのに役立ちます。
アルゴリズムの勉強としては良いですが、データ量が増えると処理速度が極端に落ちるため、実務での大量データ処理には向きません。
数件から数十件程度のデータであれば問題なく動作します。
containsを使うシンプルな方法
新しいリストを用意し、そこに要素が存在しない場合のみ追加していく手法です。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ListDuplicateCheck {
public static void main(String[] args) {
List<String> originalList = Arrays.asList("りんご", "みかん", "りんご", "バナナ");
List<String> uniqueList = new ArrayList<>();
for (String item : originalList) {
// リストに含まれていない場合のみ追加
if (!uniqueList.contains(item)) {
uniqueList.add(item);
}
}
System.out.println(uniqueList);
// 結果: [りんご, みかん, バナナ]
}
}containsを使うこのコードは直感的でわかりやすいのがメリットです。
しかし、containsメソッドは内部でリストの全要素を走査するため、データ数が増えると処理時間が長くなります。
Collections.frequencyを使った方法
Collections.frequencyを使うと、指定した要素がリスト内にいくつ存在するかを数えられます。
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class FrequencyCheck {
public static void main(String[] args) {
List<String> items = Arrays.asList("A", "B", "A", "C");
for (String item : items) {
// 出現回数が2回以上のものを探す
if (Collections.frequency(items, item) > 1) {
System.out.println(item + " は重複しています");
}
}
}
}重複している要素そのものを特定したい場合に便利です。
ただし、Collections.frequencyも内部的に全探索を行うため、データ量が増えるとパフォーマンスは低下します。
Collections.binarySearchでソート済みリストの重複を調べる方法
ここまでのcontainsやfrequencyは、リストの先頭から末尾まで全走査するため計算量はO(n)です。
しかし、リストがあらかじめソート済みであれば、Collections.binarySearchを使うことで計算量をO(log n)まで落とせます。
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class BinarySearchCheck {
public static void main(String[] args) {
// 昇順にソート済みであることが前提条件
List<Integer> sortedList = Arrays.asList(1, 3, 5, 7, 9);
int target = 5;
int index = Collections.binarySearch(sortedList, target);
boolean exists = index >= 0;
System.out.println(target + " は存在する: " + exists);
// 結果: 5 は存在する: true
}
}Collections.binarySearchは、ソート済みリストに特定の値が存在するかをO(log n)で高速に確認できます。
ただし、複数の同値要素がある場合にどのインデックスが返されるかはJDK実装依存のため(Oracle公式ドキュメントで保証外と明記されています)、重複の件数を正確にカウントする用途には向きません。
また、対象のリストが事前にソートされている必要があり、ソートされていないリストに使うと結果は保証されません。
実務では、マスタデータなど事前にソート済みであることが確定しているリストを対象にした「特定の値が含まれているかどうか」の存在確認に限定して使うのが安全です。
日常的な重複排除であれば、後述するHashSetを使う方がシンプルで速く済みます。
Setで重複を削除する実装例|HashSet・LinkedHashSet・TreeSet

重複チェックにおいて、もっともパフォーマンスが良いのがSetインターフェースを利用する方法です。
Setは数学の「集合」をモデルにしており、仕組み上、同じ値を2つ保持できません。
この特性を利用することで、複雑な判定ロジックを書かずに重複を排除できます。
HashSetで重複を排除する基本パターン
HashSetはもっとも高速なSetの実装です。順序を保証しない代わりに、非常に高速に動作します。
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
List<String> input = Arrays.asList("Java", "Python", "Java", "Ruby");
// Listをコンストラクタに渡すだけで重複が消える
Set<String> uniqueSet = new HashSet<>(input);
System.out.println(uniqueSet);
// 結果: [Ruby, Python, Java] (順序はランダム)
}
}10万件を超えるような大量データを扱う場合、Listでチェックするよりも圧倒的に高速です。
参考までに、10万件のString型データで重複排除した場合、List.containsでのループは約5秒かかります。
一方、HashSetへの変換は0.01秒程度で完了します(計測環境: Java 21, MacBook Pro M3。
2026年8月時点の最新LTSはJava 25)。
特に順序を気にする必要がない場合は、迷わずHashSetを選んでください。
LinkedHashSetで順序を保ったまま重複を除去
データの並び順を変えたくない場合は、LinkedHashSetを使用します。
入力された順番を記憶しているため、元のリストの並びを維持したまま重複だけを取り除けます。
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class LinkedHashSetExample {
public static void main(String[] args) {
List<String> input = Arrays.asList("東京", "大阪", "東京", "福岡");
Set<String> orderedSet = new LinkedHashSet<>(input);
System.out.println(orderedSet);
// 結果: [東京, 大阪, 福岡] (出現順が維持される)
}
}UI(画面)に表示するリストなど、ユーザーが見たときの並び順が重要なケースで重宝します。
TreeSetでソートしながら重複チェック
重複を排除しつつ、辞書順や昇順に並べ替えたい場合はTreeSetが最適です。
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
public class TreeSetExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 1, 3, 5, 2);
Set<Integer> sortedSet = new TreeSet<>(numbers);
System.out.println(sortedSet);
// 結果: [1, 2, 3, 5] (昇順にソートされる)
}
}内部でデータを並べ替える処理が入るため、HashSetよりは若干遅くなりますが、ソート処理を別途書く手間が省けます。
Mapで重複をカウント・抽出するにはどうする?

単に重複を取り除くだけでなく、「どのデータが何回重複しているか」を知りたいときはMapを使います。
Keyに「データそのもの」、Valueに「出現回数」を持たせるのが定石です。
HashMapで出現回数をカウントする
Java 8以降では、mergeメソッドを使うとカウントアップ処理が非常に簡潔に書けます。
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MapCountExample {
public static void main(String[] args) {
List<String> votes = Arrays.asList("A案", "B案", "A案", "C案", "A案");
Map<String, Integer> countMap = new HashMap<>();
for (String vote : votes) {
// キーが存在しなければ1、存在すれば現在の値+1
countMap.merge(vote, 1, Integer::sum);
}
System.out.println(countMap);
// 結果: {A案=3, B案=1, C案=1}
}
}この方法は、アンケートの集計や、ログファイル内のエラー回数の分析などで頻繁に使用されます。
重複した要素だけ抽出する方法
Mapで集計したあと、Valueが2以上のものだけを取り出せば、重複データリストが作成できます。
for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
if (entry.getValue() > 1) {
System.out.println("重複データ: " + entry.getKey() + " (" + entry.getValue() + "回)");
}
}エラーチェックのロジックとして、「重複している行をユーザーに知らせる」といった機能要件がある場合に有効です。
Stream APIで重複を排除する実装例

Java 8(2014年リリース)で登場したStream APIは、コレクション操作を宣言的に記述できる強力なツールです。
複雑な重複チェックロジックも、流れるようなコードで記述できます。
distinct()で重複を取り除く
distinct()はもっとも簡単な重複排除の方法です。これは内部的にHashSetのような仕組みを使って判定しています。
Stream処理の途中で挟み込むことができるため、例えば「フィルタリングしてから重複排除し、最後に大文字に変換する」といった一連の流れをスムーズに書けます。
実務では、distinct()は内部的にHashSetと同等の仕組みで動くため、単体の速度面での優位性はほとんどありません。
それでもStream APIを選ぶ場面は、フィルタリングや変換など他の処理と1本のメソッドチェーンでつなげたいときです。
逆に重複排除だけが目的なら、HashSetへの変換のほうがコードの意図が明確になります。
Collectors.groupingByで重複を数える
Mapを使ったカウント処理をStreamで書くと、以下のようになります。
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StreamGroupingExample {
public static void main(String[] args) {
List<String> items = Arrays.asList("apple", "banana", "apple");
Map<String, Long> counts = items.stream()
.collect(Collectors.groupingBy(
s -> s,
Collectors.counting()
));
System.out.println(counts);
// 結果: {banana=1, apple=2}
}
}SQLのGROUP BY句に近い感覚で操作できるため、データベースに慣れたエンジニアには非常に馴染みやすい書き方です。
Streamで重複要素のみ抽出する手法
少し応用的なテクニックですが、Setに「追加できたかどうか」を判定条件にすることで、重複要素だけを抽出できます。
Set.add()メソッドは、すでに要素が存在する場合にfalseを返す性質を利用します。
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class ExtractDuplicates {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1, 2, 3, 1, 4, 2);
Set<Integer> uniqueSet = new HashSet<>();
List<Integer> duplicates = nums.stream()
.filter(n -> !uniqueSet.add(n)) // 追加できなかった=重複
.collect(Collectors.toList());
System.out.println(duplicates);
// 結果: [1, 2]
}
}このコードは非常にスマートで、現場でも「おっ、こいつできるな」と思われるテクニックの一つです。
実務では、CSVインポート時に重複行を検出してユーザーに警告を出す処理や、注文データの二重送信チェックなどで使われます。
重複データの行番号(インデックス)を特定する方法
「何番目の要素が重複しているか」を知りたい場合、たとえばCSV取り込み時に「◯行目が重複しています」とユーザーに伝えたいケースでは、値そのものだけでなく元リストのインデックスも取得する必要があります。
IntStream.range()で添字を発行し、Set.add()がfalseを返すかどうかで重複判定しながらインデックスを集めます。
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class DuplicateIndexFinder {
public static void main(String[] args) {
List<String> rows = List.of("A案", "B案", "A案", "C案", "B案");
Set<String> seen = new HashSet<>();
List<Integer> duplicateIndexes = IntStream.range(0, rows.size())
.filter(i -> !seen.add(rows.get(i))) // 追加できなかった=重複
.boxed()
.collect(Collectors.toList());
System.out.println(duplicateIndexes);
// 結果: [2, 4](0始まりのインデックス。2行目と4行目が重複)
}
}この方法なら、重複しているという事実だけでなく「元データの何番目にあったか」まで特定できるため、CSVインポートのエラーメッセージやログ出力に活用できます。
オブジェクト(自作クラス)の重複チェックはどうする?

私が新人だった頃、顧客情報の取り込み処理でequals/hashCodeのオーバーライドを忘れ、同一顧客のデータが二重登録されてしまった経験があります。この章では、そうした落とし穴を避けるための正しい実装方法を解説します。
ここまでの例は文字列や数値などの単純なデータ型でしたが、自作のクラス(UserクラスやProductクラスなど)で重複チェックを行う場合は注意が必要です。
結論から言うと、equalsメソッドとhashCodeメソッドを必ずオーバーライドする必要があります。
これを行わないと、全く同じ値を持っていても「別のもの」として扱われてしまうからです。
オブジェクトが比較できない理由
Javaのデフォルトの動作では、オブジェクトの比較は「メモリ上のアドレスが同じか」で行われます。
User user1 = new User("田中");
User user2 = new User("田中");上記のように、中身が同じ「田中」であっても、newを使って別々に生成されたオブジェクトは、アドレスが異なるため「重複していない」と判定されます。
このアドレスの差分による誤判定が、意図しない重複登録の原因となります。
equalsとhashCodeを正しく実装する方法
重複チェックを正しく機能させるには、以下のように定義してください。
- equals: オブジェクトの中身(フィールドの値)が同じならtrueを返すように書き換える。
- hashCode: 中身が同じオブジェクトなら、同じハッシュ値を返すように書き換える。
特にHashSetやHashMapは、内部でハッシュコードを使ってデータの格納場所を決めているため、hashCodeの実装を忘れると重複排除が機能しません。
手動で実装する場合は、Java標準クラスのObjects.equals()とObjects.hash()を使うと、null安全な比較とハッシュ値の計算が簡潔に書けます。
import java.util.Objects;
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
// 同一インスタンスなら即true
if (this == o) return true;
// nullまたは型が異なる場合はfalse
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
// フィールドの値が同じであれば同一とみなす
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}equalsとhashCodeをオーバーライドすることで、HashSetに追加する際もフィールドの値をもとに重複判定が正しく行われます。
Set<User> userSet = new HashSet<>();
userSet.add(new User("田中", 30));
userSet.add(new User("田中", 30)); // 同じ値なので追加されない
System.out.println(userSet.size()); // 結果: 1ただし、フィールドが増えるたびにequals/hashCodeのコードを修正し続けるのは手間がかかりますし、修正漏れがバグの温床になります。
そこで、Lombokというライブラリを使うと、この処理を自動化できます。
データ保持だけが目的のクラスであれば、Java 16以降で正式導入されたrecordを使うことでequals/hashCodeの実装自体が不要になる選択肢もあります。
lombok @EqualsAndHashCode の使い方
Lombokというライブラリを使えば、この手間を完全に解消できます。
クラスにアノテーションをつけるだけで、自動的にメソッドを生成してくれます。
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class User {
private String name;
private int age;
// コンストラクタやGetterなどは省略
}2026年8月現在も、現場ではLombokを使うのがほぼ常識となっています。開発効率が上がるだけでなく、実装ミスを防ぐことができるからです。
なお、Java 16以降ではrecordクラスを使えば、equals/hashCodeが自動生成されます。
フィールドがすべてfinalで十分な場面では、Recordを使うことでequals/hashCodeの実装コストをゼロにできます。
大規模データにおけるJava重複チェックの注意点

データ量が数万件程度ならメモリ上で処理しても問題ありませんが、数百万、数千万件となるとメモリ管理の工夫が不可欠です。
安易にListに入れて処理すると、OutOfMemoryErrorでアプリケーションが停止します。
メモリ使用量を抑えるための設計
大量データを扱う際は、すべてのデータを一度にメモリに読み込まない工夫が必要です。
例えば、ファイルからデータを読み込みながら処理する場合、1行読み込むごとにDBに問い合わせて重複確認をするか、あるいはBloom Filterのような確率的データ構造を使ってメモリ効率よく判定する手法を検討します。
Bloom Filterは「含まれていない」は100%正確に判定でき、「含まれている」は偽陽性(false positive)が発生しうるデータ構造です。
Java向けにはGoogle GuavaライブラリのBloomFilterクラスが手軽に使えます。
並列処理(parallelStream)使用時の注意
Java 8以降のparallelStreamを使えば、マルチコアCPUを活かして処理を高速化できます。
ただし、重複チェックで並列処理を行う場合は、スレッドセーフなクラスを選びます。
通常のArrayListやHashSetはスレッドセーフではないため、並列処理中に同時に書き込みを行うとデータが壊れます。
ConcurrentHashMapのキーセットを利用するなど、並行処理に対応したクラスを選定してください。
外部データベースを使った重複チェック戦略
Javaプログラム側ですべて処理しようとせず、データベース(RDBMS)の機能を活用するのも賢い選択です。
SQLのDISTINCTキーワードを使えば、DB側で重複を排除した結果だけを取得できます。
また、テーブルの特定カラムにUNIQUE制約(ユニークインデックス)を貼っておけば、重複データをインサートしようとした瞬間にDBがエラーを返してくれるため、もっとも確実なガードとなります。
Java重複チェックに関するよくある質問
Q1. Javaで一番簡単に重複チェックする方法は?
A1. HashSetを使うのが最も簡単です。addメソッドで要素を追加するだけで、Setの性質上、自動的に重複が排除されます。ループで手動比較する必要がなく、コード量も最小限で済みます。
Q2. Listの重複チェックとSetの重複チェックの違いは?
A2. Listはデータ構造自体が重複を許すため、containsメソッドやループで手動チェックする必要があります。一方Setは重複を許さないデータ構造のため、追加するだけで自動的に重複が排除されます。処理速度もSet(特にHashSet)の方が高速です。
Q3. 自作クラス(オブジェクト)の重複チェックで注意すべきことは?
A3. equalsメソッドとhashCodeメソッドを必ずオーバーライドする必要があります。オーバーライドしないと、Javaはオブジェクトをメモリ上のアドレスで比較するため、中身が同じでも「別のもの」として扱われ、重複チェックが正しく機能しません。
Q4. 重複の出現回数をカウントするにはどうする?
A4. HashMapを使い、要素をキー、出現回数を値として集計します。Stream APIを使う場合はCollectors.groupingByとCollectors.counting()を組み合わせることで、同様の集計を簡潔に書けます。
Q5. 大量データ(数百万件以上)で重複チェックするときの注意点は?
A5. すべてのデータを一度にメモリへ読み込むとOutOfMemoryErrorのリスクがあります。Bloom Filterのような確率的データ構造を使うか、データベース側のUNIQUE制約やDISTINCTキーワードで重複排除をDBに任せる設計が有効です。
Q6. LombokでequalsとhashCodeを自動生成できる?
A6. できます。クラスに@EqualsAndHashCodeアノテーションを付けるだけで、Lombokがequalsとhashcodeメソッドをコンパイル時に自動生成します。手動実装のミスを防げるため、実務では積極的に活用されています。
Q7. Listのcontainsで重複チェックする方法は?
A7. 新しいリストを用意し、containsメソッドで「まだ追加していない要素か」を確認しながら追加する方法が基本です。ただしcontainsは内部で全要素を走査するため、データ量が多い場合はHashSetを使う方が高速です。
Q8. ソート済みリストで重複チェックを高速化できますか?
A8. 存在確認であればできます。リストが事前にソート済みであれば、Collections.binarySearchを使うことでO(log n)の速度で特定の値が含まれているかを判定できます。ただし重複が複数ある場合にどのインデックスが返されるかは保証されないため、重複の件数を正確にカウントする用途には不向きです。存在確認に限定して使うのが安全です。
まとめ:Java重複チェックは用途で最適解が変わる
重複チェックの実装方法は、目的によって最適解が変わります。
単純な排除ならHashSet、順序保持ならLinkedHashSet、出現回数の集計ならHashMapが基本の選択肢です。
重複チェックは、システム開発において避けては通れない重要な処理です。
リストを手動で回す方法から、SetやStreamを使う洗練された方法まで、選択肢はいくつもあります。
私が開発現場で後輩にアドバイスするときは、まずはSet(HashSet)を使うよう指導しています。
理由は、それがもっともバグが入りにくく、かつパフォーマンスも優れているからです。
しかし、順序の維持が必要だったり、重複の回数を知りたかったりと、要件によって最適な解は変わります。
迷ったときの判断基準は「まずHashSetで試す → 順序が必要ならLinkedHashSet → 回数が必要ならHashMap → 他の処理と組み合わせるならStream」の順です。
単にコピペで実装するのではなく、「なぜそのクラスを使うのか」を理解して選択できるようになれば、エンジニアとしてのスキルは格段に向上します。
用途別:Javaの重複チェックの最適解
これまで紹介した方法を、目的別に整理します。自分の状況に合わせて最適なものを選んでください。
高速処理が欲しい場合
単純に重複を取り除きたいだけなら、HashSetを使うのがベストです。
コードもシンプルになりますし、計算量もほぼ$O(1)$で、データ量が増えても高速に動作します。
順序も保持したい場合
入力されたデータの順番を変えたくない場合は、LinkedHashSet一択です。
HashSetよりわずかにメモリを使いますが、ユーザー体験(UX)を損なわずに重複排除ができます。
件数を集計したい場合
「何が重複しているか」「それぞれ何個あるか」を知りたい場合は、HashMapを利用します。
データ分析や、エラーデータの特定といった用途に向いています。
可読性重視
Java 8以降(2026年8月時点の最新LTSはJava 25、最新フィーチャーリリースはJava 26)の環境で、コードの読みやすさを重視するならStream APIのdistinct()です。
メソッドチェーンの一部として組み込めるため、複雑なデータ変換処理の中で自然に重複排除を行えます。











