こんにちは。現役Javaエンジニアとして10年以上、大規模な業務システムの開発に携わってきました。
システム開発の現場では、データの整合性を保つために「重複チェック」が欠かせません。私が新人だった頃、顧客情報の取り込み処理で重複排除を忘れてしまうという失敗をした経験があります。そのときは上司にひどく怒られましたが、おかげでデータのユニーク性(一意性)を保証する重要さを学びました。
この記事を読めば、Javaにおける重複チェックの具体的な実装パターンと、状況に応じた最適な選び方がわかります。
Javaの重複チェックとは?

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

Javaには重複をチェックするための手段がいくつか用意されています。
もっとも効率的で推奨されるのは「Set」を使う方法です。理由は、Setというデータ構造自体が「重複を許さない」という性質を持っているため、追加するだけで自動的に重複排除ができるからです。しかし、要件によってはListやStream APIを使うべき場面もあります。
ここでは代表的な4つの手法を紹介します。
それぞれの特徴を理解し、使い分けるスキルが求められます。
Listの重複チェックの実装例

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メソッドは内部でリストの全要素を走査するため、データ数が増えると処理時間が長くなります。
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 + " は重複しています");
}
}
}
}重複している要素そのものを特定したい場合に便利です。ただし、これも内部的に全探索を行うため、パフォーマンス面では注意が必要です。
Stream.distinct()を使った方法
Java8から利用可能になったStream APIを使えば、驚くほど短く記述できます。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamDistinctExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 1, 4);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(distinctNumbers);
// 結果: [1, 2, 3, 4]
}
}現代のJava開発においては、この書き方が標準的になりつつあります。コードの意図が「重複排除(distinct)」であることが明確に伝わるからです。
Setを使った重複チェックの実装例

重複チェックにおいて、もっともパフォーマンスが良いのが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でチェックするよりも圧倒的に高速です。特に順序を気にする必要がない場合は、迷わずこれを選んでください。
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で出現回数をカウントする
Java8以降では、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で重複チェックするパターン

Java8で登場したStream APIは、コレクション操作を宣言的に記述できる強力なツールです。複雑な重複チェックロジックも、流れるようなコードで記述できます。
distinct()で重複を取り除く
先ほどListの章でも紹介しましたが、distinct()はもっとも簡単な方法です。これは内部的にHashSetのような仕組みを使って判定しています。
Stream処理の途中で挟み込むことができるため、例えば「フィルタリングしてから重複排除し、最後に大文字に変換する」といった一連の流れをスムーズに書けます。
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]
}
}このコードは非常にスマートで、現場でも「おっ、こいつできるな」と思われるテクニックの一つです。
オブジェクトの重複チェック(equals/hashCode)

ここまでの例は文字列や数値などの単純なデータ型でしたが、自作のクラス(UserクラスやProductクラスなど)で重複チェックを行う場合は注意が必要です。
結論から言うと、equalsメソッドとhashCodeメソッドを必ずオーバーライドする必要があります。これを行わないと、全く同じ値を持っていても「別のもの」として扱われてしまうからです。
オブジェクトが比較できない理由
Javaのデフォルトの動作では、オブジェクトの比較は「メモリ上のアドレスが同じか」で行われます。
User user1 = new User("田中");
User user2 = new User("田中");上記のように、中身が同じ「田中」であっても、newを使って別々に生成されたオブジェクトは、アドレスが異なるため「重複していない」と判定されます。これが意図しない重複登録の原因となります。
equalsとhashCodeを正しく実装する方法
重複チェックを正しく機能させるには、以下のように定義する必要があります。
- equals: オブジェクトの中身(フィールドの値)が同じならtrueを返すように書き換える。
- hashCode: 中身が同じオブジェクトなら、同じハッシュ値を返すように書き換える。
特にHashSetやHashMapは、内部でハッシュコードを使ってデータの格納場所を決めているため、hashCodeの実装を忘れると重複排除が機能しません。
lombok @EqualsAndHashCode の使い方
これらのメソッドを手動で書くのは大変ですし、フィールドが増えるたびに修正するのはバグの温床です。そこで、Lombok(ロンボク)というライブラリを使います。
クラスにアノテーションをつけるだけで、自動的にメソッドを生成してくれます。
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class User {
private String name;
private int age;
// コンストラクタやGetterなどは省略
}現場ではLombokを使うのがほぼ常識となっています。開発効率が上がるだけでなく、実装ミスを防ぐことができるからです。
大規模データにおけるJava重複チェックの注意点

データ量が数万件程度ならメモリ上で処理しても問題ありませんが、数百万、数千万件となると話が変わります。
安易にListに入れて処理しようとすると、OutOfMemoryErrorでアプリケーションが停止する恐れがあります。
メモリ使用量を抑えるための設計
大量データを扱う際は、すべてのデータを一度にメモリに読み込まない工夫が必要です。
例えば、ファイルからデータを読み込みながら処理する場合、1行読み込むごとにDBに問い合わせて重複確認をするか、あるいはBloom Filter(ブルームフィルタ)のような確率的データ構造を使ってメモリ効率よく判定する手法を検討します。
並列処理(parallelStream)使用時の注意
Java8のparallelStreamを使えば、マルチコアCPUを活かして処理を高速化できます。しかし、重複チェックにおいて並列処理を行う場合はスレッドセーフに気を配る必要があります。
通常のArrayListやHashSetはスレッドセーフではないため、並列処理中に同時に書き込みを行うとデータが壊れる可能性があります。ConcurrentHashMapのキーセットを利用するなど、並行処理に対応したクラスを選定してください。
外部データベースを使った重複チェック戦略
Javaプログラム側ですべて処理しようとせず、データベース(RDBMS)の機能を活用するのも賢い選択です。
SQLのDISTINCTキーワードを使えば、DB側で重複を排除した結果だけを取得できます。また、テーブルの特定カラムにUNIQUE制約(ユニークインデックス)を貼っておけば、重複データをインサートしようとした瞬間にDBがエラーを返してくれるため、もっとも確実なガードとなります。
まとめ:Javaの重複チェックは用途でベストな方法が変わる
今回は、Javaにおける重複チェックの具体的な実装方法と、それぞれの使い分けについて解説しました。
重複チェックは、システム開発において避けては通れない重要な処理です。
リストを手動で回す方法から、SetやStreamを使う洗練された方法まで、選択肢はいくつもあります。
私が開発現場で後輩にアドバイスするときは、まずは「Set(HashSet)」を使うよう指導しています。理由は、それがもっともバグが入りにくく、かつパフォーマンスも優れているからです。
しかし、順序の維持が必要だったり、重複の回数を知りたかったりと、要件によって最適な解は変わります。
単にコピペで実装するのではなく、「なぜそのクラスを使うのか」を理解して選択できるようになれば、エンジニアとしてのスキルは格段に向上します。
用途別:Javaの重複チェックの最適解
これまで紹介した方法を、目的別に整理します。自分の状況に合わせて最適なものを選んでください。
高速処理が欲しい場合
単純に重複を取り除きたいだけなら、HashSetを使うのがベストです。
コードもシンプルになりますし、計算量もほぼ$O(1)$で、データ量が増えても高速に動作します。
順序も保持したい場合
入力されたデータの順番を変えたくない場合は、LinkedHashSet一択です。
HashSetよりわずかにメモリを使いますが、ユーザー体験(UX)を損なわずに重複排除ができます。
件数を集計したい場合
「何が重複しているか」「それぞれ何個あるか」を知りたい場合は、HashMapを利用します。
データ分析や、エラーデータの特定といった用途に向いています。
可読性重視
Java8以降の環境で、コードの読みやすさを重視するならStream APIのdistinct()です。
メソッドチェーンの一部として組み込めるため、複雑なデータ変換処理の中で自然に重複排除を行えます。