「集合演算」は“重複なしの集まり”同士を比べる道具
Java の Set は、「重複を許さない集まり」です。HashSet や LinkedHashSet、TreeSet などが代表ですね。
業務では、
「A の権限セットと B の権限セットの共通部分」
「新しいデータと既存データの差分」
「A にはあるが B にはない ID」
といった“集合同士の比較”が頻繁に出てきます。
これを数学でいう「集合演算」として整理しておくと、
コードがシンプルになり、意図も伝わりやすくなります。
ここでは、
和集合・積集合・差集合・対称差を、
Java コレクションでどう書くかをかみ砕いて説明します。
基本の集合演算4つを Java でどう書くか
和集合:A ∪ B(どちらかに含まれるもの全部)
「A または B に含まれる要素の集まり」です。
Java では、addAll を使います。
import java.util.HashSet;
import java.util.Set;
public class UnionExample {
public static void main(String[] args) {
Set<String> a = new HashSet<>();
a.add("A");
a.add("B");
Set<String> b = new HashSet<>();
b.add("B");
b.add("C");
Set<String> union = new HashSet<>(a); // A のコピー
union.addAll(b); // B を全部足す
System.out.println(union); // [A, B, C](順序は実装依存)
}
}
Javaここでの重要ポイントは、
「元の集合を壊さないように、まずコピーを作ってから addAll している」ことです。a や b をそのまま addAll でいじると、元データが変わってしまいます。
積集合:A ∩ B(両方に含まれるもの)
「A にも B にも含まれる要素の集まり」です。
Java では、retainAll を使います。
import java.util.HashSet;
import java.util.Set;
public class IntersectionExample {
public static void main(String[] args) {
Set<String> a = new HashSet<>();
a.add("A");
a.add("B");
Set<String> b = new HashSet<>();
b.add("B");
b.add("C");
Set<String> intersection = new HashSet<>(a);
intersection.retainAll(b); // 共通しているものだけ残す
System.out.println(intersection); // [B]
}
}
Javaここでの重要ポイントは、
「retainAll は“残す”メソッドであり、“削る”メソッドではない」
という感覚です。
「B に含まれていない要素を全部削る」
=「B に含まれている要素だけ残す」
という動きになります。
差集合:A − B(A にだけ含まれるもの)
「A にはあるが B にはない要素の集まり」です。
Java では、removeAll を使います。
import java.util.HashSet;
import java.util.Set;
public class DifferenceExample {
public static void main(String[] args) {
Set<String> a = new HashSet<>();
a.add("A");
a.add("B");
Set<String> b = new HashSet<>();
b.add("B");
b.add("C");
Set<String> diff = new HashSet<>(a);
diff.removeAll(b); // B に含まれるものを全部削る
System.out.println(diff); // [A]
}
}
Javaここでの重要ポイントは、
「removeAll は“B にもあるもの”を削る=“A にしかないもの”が残る」
という理解です。
業務でよくある「新旧データの差分」は、
この差集合の考え方でそのまま書けます。
対称差:A △ B(どちらか一方にだけ含まれるもの)
「A か B のどちらかにはあるが、両方にはない要素の集まり」です。
Java には専用メソッドはないので、組み合わせて書きます。
やり方はいくつかありますが、
分かりやすい形を一つ紹介します。
import java.util.HashSet;
import java.util.Set;
public class SymmetricDifferenceExample {
public static void main(String[] args) {
Set<String> a = new HashSet<>();
a.add("A");
a.add("B");
Set<String> b = new HashSet<>();
b.add("B");
b.add("C");
// A − B
Set<String> onlyA = new HashSet<>(a);
onlyA.removeAll(b); // [A]
// B − A
Set<String> onlyB = new HashSet<>(b);
onlyB.removeAll(a); // [C]
// 和集合を取る
Set<String> symmetricDiff = new HashSet<>(onlyA);
symmetricDiff.addAll(onlyB); // [A, C]
System.out.println(symmetricDiff);
}
}
Javaここでの重要ポイントは、
「対称差=(A − B) ∪ (B − A)」
という分解を覚えておくことです。
「片方にだけあるもの全部」という意味なので、
差集合と和集合の組み合わせで自然に表現できます。
業務っぽい例1:権限セットの比較
「追加された権限」「削除された権限」を出す
ユーザーの権限が変更されたとき、
「前回から何が増えて、何が減ったか」を知りたい場面を考えます。
import java.util.HashSet;
import java.util.Set;
public class PermissionDiffExample {
public static void main(String[] args) {
Set<String> before = new HashSet<>();
before.add("READ");
before.add("WRITE");
Set<String> after = new HashSet<>();
after.add("READ");
after.add("EXECUTE");
// 追加された権限 = after − before
Set<String> added = new HashSet<>(after);
added.removeAll(before); // [EXECUTE]
// 削除された権限 = before − after
Set<String> removed = new HashSet<>(before);
removed.removeAll(after); // [WRITE]
System.out.println("追加された権限: " + added);
System.out.println("削除された権限: " + removed);
}
}
Javaここでの重要ポイントは二つです。
一つ目は、「差集合が“変更点”を表現するのにとても便利」だということ。added と removed をログに出したり、画面に表示したりできます。
二つ目は、「元の集合を壊さないように、必ずコピーを作ってから演算している」こと。new HashSet<>(after) のような「防御的コピー」は、実務ではかなり重要な習慣です。
業務っぽい例2:新旧 ID リストの差分チェック
「新しく追加された ID」「もう存在しない ID」
例えば、
「前回のバッチで処理した ID セット」と
「今回 DB から取得した ID セット」を比べるケースです。
import java.util.HashSet;
import java.util.Set;
public class IdDiffExample {
public static void main(String[] args) {
Set<Long> oldIds = new HashSet<>();
oldIds.add(1L);
oldIds.add(2L);
oldIds.add(3L);
Set<Long> newIds = new HashSet<>();
newIds.add(2L);
newIds.add(3L);
newIds.add(4L);
// 新規追加された ID = newIds − oldIds
Set<Long> added = new HashSet<>(newIds);
added.removeAll(oldIds); // [4]
// もう存在しない ID = oldIds − newIds
Set<Long> removed = new HashSet<>(oldIds);
removed.removeAll(newIds); // [1]
System.out.println("新規: " + added);
System.out.println("削除: " + removed);
}
}
Javaここでのポイントは、
「集合演算を使うと、“差分検出”のロジックが一気にシンプルになる」
ということです。
ループを二重に回して「含まれているかどうか」を手でチェックするより、removeAll 一発の方が読みやすく、バグも入りにくくなります。
Set を使うときに意識したいこと
equals / hashCode と「重複」の関係
HashSet などの Set は、
「要素の equals と hashCode が同じなら“同じもの”とみなす」
というルールで重複を判定します。
独自クラスを Set に入れるときは、equals / hashCode を正しく実装しておくことがとても重要です。
例えば、「ユーザーIDが同じなら同一ユーザーとみなす」なら、id フィールドだけで equals / hashCode を定義します。
これをサボると、
「同じユーザーが Set に二重に入る」
「集合演算の結果がおかしくなる」
といった問題が起きます。
まとめ:集合演算で身につけてほしい感覚
集合演算は、
単に「数学っぽいお勉強」ではなく、
「差分・共通部分・追加・削除を、短く正確に書くための道具」 です。
和集合(addAll)で「全部まとめる」。
積集合(retainAll)で「共通部分だけ残す」。
差集合(removeAll)で「片方にしかないもの」を取り出す。
対称差は「(A − B) ∪ (B − A)」として組み立てる。
元の集合を壊さないように、必ずコピーを作ってから演算する。
あなたのコードのどこかに、
「二つのリストを for で回して、手作業で差分や共通部分を計算している」箇所があれば、
そこを一度「Set + 集合演算」に置き換えられないか眺めてみてください。
それが、「データの“集まり”を数学的に捉えて、短く正確に書けるエンジニア」への、
いい一歩になります。
