1. オペレーション union(集合の和集合)
Java では集合を表すのにインターフェース Set<T> を使います。List と異なり、Set は要素の一意性を保証し、通常は順序を気にしません(ただし LinkedHashSet のような特別な実装を使う場合を除く)。代表的な実装は HashSet と TreeSet です。主な役割は、要素の存在判定を高速に行い、重複をなくすことです。
Set が必要になる場面
- 一意性が重要なとき。例: サイトを訪れたユニークなユーザーの一覧。
- 要素の存在を素早く確認したいとき。HashSet の contains は通常定数時間で動作します。
- 集合の基本操作(和集合・積集合・差集合)を行いたいとき。
Union は 2 つ以上の集合の和です。結果には両方(またはすべて)の集合に含まれる要素が重複なしで入ります。
実例
たとえば、「ロボット工学」と「プログラミング」のクラブに参加している学生の 2 つの集合があるとします。
Set<String> robotics = Set.of("アーニャ", "ボリス", "ヴィカ");
Set<String> programming = Set.of("ヴィカ", "グレブ", "ダーシャ");
少なくともどちらか一方のクラブに通っている学生すべての集合を得たいとします。
Stream API での解法
最も素直な方法は、2 つのストリームを結合して Set に収集することです。
Set<String> all = Stream.concat(
robotics.stream(),
programming.stream()
).collect(Collectors.toSet());
System.out.println(all); // [アーニャ, ボリス, ヴィカ, グレブ, ダーシャ]
解説:
- Stream.concat は 2 つのストリームを結合します。
- collect(Collectors.toSet()) は要素を集合に収集し(重複を自動的に排除します)、結果を返します。
代替案: 2 個を超える集合の場合
3 個以上のクラブがある場合は、Stream.of と flatMap を使います。
Set<String> math = Set.of("ジェーニャ", "ヴィカ", "ボリス");
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [アーニャ, ボリス, ヴィカ, グレブ, ダーシャ, ジェーニャ]
なぜ Set なのか?
Set は重複を自動的に取り除くからです。List に収集すると、同じ名前が複数回現れます。
2. オペレーション intersection(集合の共通部分/積集合)
Intersection は、両方の集合に同時に存在する要素の集合です。
実例
「ロボット工学」と「プログラミング」の両方に参加している学生を見つけます。
Set<String> robotics = Set.of("アーニャ", "ボリス", "ヴィカ");
Set<String> programming = Set.of("ヴィカ", "グレブ", "ダーシャ");
Stream API での解法:
Set<String> both = robotics.stream()
.filter(programming::contains)
.collect(Collectors.toSet());
System.out.println(both); // [ヴィカ]
解説:
「ロボット工学」の全参加者を走査し、「プログラミング」にもいる人だけをフィルターします。結果は、両方のクラブにいる名前の集合です。
代替方法(Stream API なし)
組み込みメソッド retainAll を使うこともできます(これは現在の集合を変更します)。
Set<String> intersection = new HashSet<>(robotics);
intersection.retainAll(programming);
System.out.println(intersection); // [ヴィカ]
ただし、この講義の文脈では Stream API に焦点を当てます。
3. オペレーション difference(集合の差)
Difference は、最初の集合にあって 2 番目の集合にない要素の集合です。
実例
「ロボット工学」にのみ参加し、「プログラミング」には参加していない学生を見つけます。
Set<String> robotics = Set.of("アーニャ", "ボリス", "ヴィカ");
Set<String> programming = Set.of("ヴィカ", "グレブ", "ダーシャ");
Stream API での解法:
Set<String> onlyRobotics = robotics.stream()
.filter(name -> !programming.contains(name))
.collect(Collectors.toSet());
System.out.println(onlyRobotics); // [アーニャ, ボリス]
解説:
「ロボット工学」の参加者から、「プログラミング」にいない人だけを残します。
代替方法(Stream API なし)
Set<String> difference = new HashSet<>(robotics);
difference.removeAll(programming);
System.out.println(difference); // [アーニャ, ボリス]
4. 実践課題: ユーザー(学生)リストの処理
課題 1: どちらか一方のクラブにだけ通っている学生を見つける
「ロボット工学」か「プログラミング」のどちらか一方にだけ通い、両方には通っていない学生を特定します。これは対称差(集合に対する xor)です。
Set<String> onlyOne = Stream.concat(
robotics.stream().filter(name -> !programming.contains(name)),
programming.stream().filter(name -> !robotics.contains(name))
).collect(Collectors.toSet());
System.out.println(onlyOne); // [アーニャ, ボリス, グレブ, ダーシャ]
課題 2: 複数クラブからすべてのユニークな学生を集める
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [アーニャ, ボリス, ヴィカ, グレブ, ダーシャ, ジェーニャ]
課題 3: どのクラブにも通っていない学生を見つける
クラス全員のリストがあるとします。
Set<String> allStudents = Set.of("アーニャ", "ボリス", "ヴィカ", "グレブ", "ダーシャ", "ジェーニャ", "イーゴリ", "カーチャ");
どのクラブにも通っていない人を特定します。
Set<String> attendees = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
Set<String> notInAny = allStudents.stream()
.filter(name -> !attendees.contains(name))
.collect(Collectors.toSet());
System.out.println(notInAny); // [イーゴリ, カーチャ]
5. 重要な注意: equals、hashCode とパフォーマンス
なぜ equals と hashCode を正しく実装することが重要なのか?
Set(集合)でのあらゆる操作は、equals と hashCode の正しさに依存します。独自クラス(例: Student)のオブジェクトを格納する場合は、必ずこれらのメソッドをオーバーライドしてください。そうしないと比較が正しく機能しません。
例:
class Student {
String name;
int age;
// equals と hashCode を必ずオーバーライドしましょう!
}
これを行わないと、同じ名前と年齢の 2 人の学生でも、Set にとっては別オブジェクトとして扱われます。
なぜ List ではなく Set を使う方が良いのか?
- Set の contains は高速(通常は定数時間)です。
- List では要素検索が線形時間になり、大きなコレクションでは致命的になり得ます。
- 集合演算(union、intersection、difference)には Set の方が効率的かつ理にかなっています。
6. 集合操作でよくあるミス
ミス 1: 集合演算に Set の代わりに List を使う。 List に収集すると重複は削除されず、contains も遅いです。union/intersection/difference には Set を使いましょう。
ミス 2: オブジェクトに対して equals/hashCode を実装していない。 独自クラスのオブジェクトを Set に格納しているのに、equals と hashCode をオーバーライドしていないと、積集合や差集合の結果が「おかしく」なります。意味的に同じオブジェクトでも等しいと判定されません。
ミス 3: ストリーム処理中にコレクションを変更する。 ストリームの最中に元の Set を変更(追加・削除)しようとすると、ConcurrentModificationException が発生します。常に新しい集合で作業しましょう。
ミス 4: 順序が保持されないことに気づいていない。 HashSet は要素の順序を保証しません。順序が重要な場合は、LinkedHashSet や TreeSet を使用してください。
ミス 5: 2 個を超えるコレクションに Stream.concat を使う。 Stream.concat は 2 つのストリームしか結合できません。より多い場合は Stream.of(...) と flatMap を使いましょう。
ミス 6: null の扱いを誤る。 多くの集合は null 値を好みません。特に Set.of(...) は null を許容しません。null を扱う場合は別の実装を使うか、事前にフィルタリングしてください。
GO TO FULL VERSION