JavaのListで重複要素を削除する方法を徹底解説!Set変換とdistinctの使い分け
生徒
「JavaのListを使ってデータを管理しているのですが、中身に同じ値が混ざってしまいました。この重複した要素をきれいに取り除く方法はありますか?」
先生
「Javaには重複を許さない『Set』という仕組みや、最新の『Stream API』を使った便利な方法が用意されていますよ。用途に合わせて使い分けるのがコツです。」
生徒
「SetとStream、どちらを使えばいいのか迷いそうです。具体的なプログラムの書き方や違いを教えていただけますか?」
先生
「もちろんです!初心者の方でも分かりやすいように、順番に解説していきますね。それでは一緒に見ていきましょう!」
1. JavaのListで重複が発生する理由と削除の重要性
Javaプログラミングにおいて、ArrayListなどのListインターフェースを実装したクラスは非常に頻繁に利用されます。リストは要素の順序を保持し、同じ値を複数格納できるという便利な特徴を持っています。しかし、データベースから取得したデータやユーザーが入力した値、ファイルから読み込んだログなどを処理する際、意図せず同じデータが重複して混入してしまうことがあります。
重複したデータが残っていると、例えば計算結果が狂ってしまったり、画面に同じ名前が何度も表示されてしまったりと、アプリケーションの品質を下げる原因になります。そのため、リストから重複を排除して「一意(ユニーク)なデータ」のみを抽出する技術は、Javaエンジニアにとって必須のスキルと言えます。今回は、初心者でもすぐに使える「HashSetへの変換」と、モダンな書き方である「Stream APIのdistinct」の2つを中心に詳しく解説します。
2. HashSetを使って簡単に重複を削除する方法
最も基本的で古くから使われている手法が、Set(集合)を利用する方法です。JavaのSetインターフェースを継承しているHashSetクラスは、「重複した要素を持つことができない」という強力な特性を持っています。この性質を利用して、一度リストをセットに変換し、再びリストに戻すだけで、驚くほど簡単に重複を消し去ることができます。
この方法の最大のメリットは、コードが非常に短く、直感的に理解しやすい点にあります。ただし、HashSetを使うと、元のリストが持っていた「要素の並び順」がバラバラになってしまうという注意点があります。順序を気にする必要がある場合は、後述する別の方法を検討しましょう。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ListDuplicateRemoveSet {
public static void main(String[] args) {
// 重複を含んだリストを作成
List<String> names = new ArrayList<>(Arrays.asList("田中", "佐藤", "鈴木", "田中", "高橋", "佐藤"));
System.out.println("元のリスト: " + names);
// HashSetに変換して重複を削除
Set<String> set = new HashSet<>(names);
// 再びListに戻す
List<String> uniqueNames = new ArrayList<>(set);
System.out.println("重複削除後のリスト: " + uniqueNames);
}
}
元のリスト: [田中, 佐藤, 鈴木, 田中, 高橋, 佐藤]
重複削除後のリスト: [佐藤, 鈴木, 田中, 高橋]
3. 順序を維持しながら重複を削除するLinkedHashSet
先ほどのHashSetでは、データの並び順が変わってしまうという欠点がありました。もし、「重複は消したいけれど、最初に入力した順番はそのまま守りたい」という場合には、LinkedHashSetを使用するのが最適解です。
LinkedHashSetは、要素の重複を許さないというSetの機能に加え、要素が追加された順番を内部で記録してくれる仕組みを持っています。これにより、元のリストの順序を崩さずに、重複した二回目以降のデータだけを効率的に取り除くことが可能になります。ユーザーインターフェースで表示順が重要な場合などに多用されるテクニックです。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class ListOrderPreserved {
public static void main(String[] args) {
// 重複のあるリスト(追加順が重要)
List<Integer> numbers = new ArrayList<>(Arrays.asList(10, 20, 30, 10, 40, 20, 50));
System.out.println("変換前: " + numbers);
// LinkedHashSetを使って順序を保持したまま重複削除
Set<Integer> set = new LinkedHashSet<>(numbers);
List<Integer> uniqueNumbers = new ArrayList<>(set);
System.out.println("変換後: " + uniqueNumbers);
}
}
変換前: [10, 20, 30, 10, 40, 20, 50]
変換後: [10, 20, 30, 40, 50]
4. Stream APIのdistinctメソッドでスマートに削除する
Java 8以降で導入された「Stream API」を利用すると、より宣言的でモダンなコードを書くことができます。リストに対してstream()を呼び出し、その後にdistinct()メソッドを繋げるだけで、重複排除の処理が完了します。最後にcollect(Collectors.toList())を使って再びリスト形式にまとめます。
この方法の素晴らしい点は、元のリストを変更せずに、新しいリストとして結果を取得できる(不変性の保持)点にあります。また、フィルタリングや加工といった他の処理と組み合わせるのが非常に簡単です。例えば、「重複を削除した後に、特定の文字を含むものだけを抽出して、名前順に並べ替える」といった一連の流れを、たった数行のメソッドチェーンで記述できるようになります。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamDistinctExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("apple", "orange", "banana", "apple", "cherry", "orange");
// Stream APIを使用して重複を排除
List<String> distinctFruits = fruits.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("加工後のリスト: " + distinctFruits);
}
}
加工後のリスト: [apple, orange, banana, cherry]
5. 大きなデータセットにおけるパフォーマンスの比較
重複削除の方法を選択する際、扱うデータの量によっては実行速度(パフォーマンス)を意識する必要があります。数件から数百件程度のリストであれば、どの方法を使っても体感できるほどの差はありません。しかし、数万件、数百万件という大規模なデータを扱う業務システムやデータ分析の現場では、内部的な仕組みの違いが処理時間に影響します。
一般的に、HashSetへの変換は非常に高速です。ハッシュアルゴリズムを利用して重複をチェックするため、データ量が増えても検索効率が落ちにくいのが特徴です。一方で、Stream APIのdistinct()も内部的には同様の仕組みを使っていますが、ストリームの生成やパイプライン処理のオーバーヘッドがわずかに発生する場合があります。可読性を優先するならStream、極限まで速度を追求するならSet変換という使い分けが一般的ですが、現代のJava開発では読みやすさを重視してStream APIが選ばれる場面が増えています。
6. 自作クラスの重複を削除する際の注意点
これまでの例ではStringやIntegerといった標準クラスを使ってきましたが、自分で作成したクラス(例えばUserクラスやProductクラス)のオブジェクトをリストに格納している場合は、少し注意が必要です。Javaが「二つのオブジェクトが同じものである」と判定するためには、そのクラス内でequals()メソッドとhashCode()メソッドを正しくオーバーライド(再定義)していなければなりません。
もしこれらのメソッドが実装されていないと、たとえ中身のIDや名前が全く同じであっても、Javaはそれらを「メモリ上の別の場所にある別物」と判断してしまい、重複削除が正しく機能しません。自作クラスを扱う際は、IDE(EclipseやIntelliJ IDEA)の自動生成機能を使って、適切な比較ロジックを実装することを忘れないようにしましょう。これはJavaのコレクションフレームワークを使いこなす上での重要なポイントです。
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
class Member {
private int id;
private String name;
public Member(int id, String name) {
this.id = id;
this.name = name;
}
// equalsとhashCodeを正しく実装することで重複判定が可能になる
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return id == member.id && Objects.equals(name, member.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return name;
}
}
public class CustomObjectDistinct {
public static void main(String[] args) {
List<Member> members = Arrays.asList(
new Member(1, "山田"),
new Member(2, "鈴木"),
new Member(1, "山田") // 重複
);
List<Member> uniqueMembers = members.stream()
.distinct()
.collect(Collectors.toList());
System.out.println("一意なメンバー: " + uniqueMembers);
}
}
一意なメンバー: [山田, 鈴木]
7. どの方法を選ぶべき?状況別の使い分けガイド
ここまで紹介した手法をどのように使い分けるべきか、基準を整理してみましょう。まず、もっとも汎用的で推奨されるのは「Stream APIのdistinct()」です。コードがスッキリとし、その後のソートや加工処理へスムーズに繋げられるため、チーム開発においても意図が伝わりやすいという利点があります。
一方で、非常に古いバージョンのJava環境で保守を行っている場合や、とにかくシンプルに「ListをSetに入れて戻すだけ」という定型処理で済ませたい場合はHashSetの利用が適しています。順序を死守したいという明確な要件があるならば、迷わずLinkedHashSetを選択してください。また、NULL要素がリストに含まれている場合の挙動にも注意しましょう。多くのSet実装はNULLを許容しますが、Stream処理の中で要素のプロパティを参照して比較を行うような複雑なケースでは、事前にfilter(Objects::nonNull)を挟むなどの工夫が必要になることもあります。
8. 実践で役立つ!重複削除と同時に行うフィルタリング
実際の開発現場では、単に重複を消すだけでなく、「特定の条件に合うものだけを残して、さらに重複を消す」という複合的な操作が求められます。これを実現するにはStream APIが最も輝きます。例えば、文字列のリストから空文字を除去し、すべてを大文字に変換した上で、重複を排除するといった処理です。
このような処理を従来のfor文やif文で書こうとすると、一時的なリストを作成したり、何度もループを回したりと、非常に煩雑なコードになってしまいます。しかし、Stream APIを使えばパイプラインを組み立てるように記述できるため、バグが混入しにくく、メンテナンス性の高いプログラムになります。初心者の方は、まず基本のdistinct()をマスターし、徐々に他のメソッドとの組み合わせに慣れていくのが、Java習得への近道です。