Java Tips | コレクション:集合演算

Java Java
スポンサーリンク

「集合演算」は“重複なしの集まり”同士を比べる道具

Java の Set は、「重複を許さない集まり」です。
HashSetLinkedHashSetTreeSet などが代表ですね。

業務では、
「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 している」ことです。
ab をそのまま 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

ここでの重要ポイントは二つです。

一つ目は、「差集合が“変更点”を表現するのにとても便利」だということ。
addedremoved をログに出したり、画面に表示したりできます。

二つ目は、「元の集合を壊さないように、必ずコピーを作ってから演算している」こと。
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 は、
「要素の equalshashCode が同じなら“同じもの”とみなす」
というルールで重複を判定します。

独自クラスを Set に入れるときは、
equals / hashCode を正しく実装しておくことがとても重要です。

例えば、「ユーザーIDが同じなら同一ユーザーとみなす」なら、
id フィールドだけで equals / hashCode を定義します。

これをサボると、
「同じユーザーが Set に二重に入る」
「集合演算の結果がおかしくなる」
といった問題が起きます。


まとめ:集合演算で身につけてほしい感覚

集合演算は、
単に「数学っぽいお勉強」ではなく、
「差分・共通部分・追加・削除を、短く正確に書くための道具」 です。

和集合(addAll)で「全部まとめる」。
積集合(retainAll)で「共通部分だけ残す」。
差集合(removeAll)で「片方にしかないもの」を取り出す。
対称差は「(A − B) ∪ (B − A)」として組み立てる。
元の集合を壊さないように、必ずコピーを作ってから演算する。

あなたのコードのどこかに、
「二つのリストを for で回して、手作業で差分や共通部分を計算している」箇所があれば、
そこを一度「Set + 集合演算」に置き換えられないか眺めてみてください。

それが、「データの“集まり”を数学的に捉えて、短く正確に書けるエンジニア」への、
いい一歩になります。

タイトルとURLをコピーしました