コレクション間の差分(removeAll, retainAll) — 差分集計
差分を取りたいときの定番が removeAll(差集合の除去)と retainAll(共通部分の保持)。どちらも「破壊的(元のコレクションを直接変更)」なので、用途に合わせて安全に使い分けます。セット演算の直感(差・積)で捉えると迷いません。
基本の考え方と違い
- removeAll(差集合の除去):
- 意味: this から other に含まれる要素を全部削る → 結果は「this − other」。
- 戻り値: 変更があれば true。なければ false。
- 注意: 元を直接変更する(破壊的)。コピーが必要なら new ArrayList<>(list) してから使う。
- retainAll(共通部分の保持):
- 意味: this を「other に含まれる要素だけ」にする → 結果は「this ∩ other」。
- 戻り値: 変更があれば true。
- 注意: 差分ではなく「交差の抽出」。フィルタを自前で書くより簡潔・高速。
- equals に依存:
- ポイント: 要素一致は equals 判定。独自型は equals/hashCode を適切に実装すること。
すぐ試せる基本例
差集合(A − B)を作る(removeAll)
import java.util.*;
List<Integer> A = new ArrayList<>(List.of(1, 2, 3, 4, 5));
List<Integer> B = List.of(2, 4);
// AからBの要素を削る → 差集合
A.removeAll(B);
System.out.println(A); // [1, 3, 5]
Java- ねらい: Aにしかない要素を残したいとき。破壊的なので A を保存したいなら A のコピーを作ってから実施。
共通部分(A ∩ B)を抽出(retainAll)
import java.util.*;
List<String> A = new ArrayList<>(List.of("aaa", "bbb", "ccc"));
List<String> B = List.of("aaa", "eee");
// Aを「Bにある要素だけ」にする → 交差
A.retainAll(B);
System.out.println(A); // [aaa]
Java- ねらい: 2集合の重なりだけ欲しいとき。ループ+containsより短く、安全。
例題で身につける
例題1: 新規ユーザーの抽出(差集合)
List<String> today = new ArrayList<>(List.of("u1","u2","u3","u4"));
List<String> existing = List.of("u2","u4","u5");
// 今日のアクセスから既存ユーザーを除く → 新規のみ
today.removeAll(existing);
System.out.println(today); // [u1, u3]
Java- ポイント: 「Aにしかない」を取りたいなら removeAll が直感的。
例題2: 配信対象の絞り込み(交差)
List<String> candidates = new ArrayList<>(List.of("tokyo","osaka","kyoto"));
List<String> subscribed = List.of("tokyo","nagoya");
// 購読者に含まれる都市のみ残す
candidates.retainAll(subscribed);
System.out.println(candidates); // [tokyo]
Java- ポイント: retainAll は「フィルタ関数の代わりに使う交差」。
例題3: 左右の差分を両側で取得(対称差の簡易計算)
List<Integer> A = List.of(1,2,3,4);
List<Integer> B = List.of(3,4,5,6);
List<Integer> onlyA = new ArrayList<>(A);
onlyA.removeAll(B); // [1,2]
List<Integer> onlyB = new ArrayList<>(B);
onlyB.removeAll(A); // [5,6]
// 対称差
List<Integer> symDiff = new ArrayList<>();
symDiff.addAll(onlyA);
symDiff.addAll(onlyB);
System.out.println(symDiff); // [1, 2, 5, 6]
Java- ポイント: 破壊的なのでコピーしてから操作すると安全。
実用レシピとテンプレート
- 差集合(非破壊で返す)
static <T> List<T> difference(Collection<T> a, Collection<T> b) {
List<T> result = new ArrayList<>(a);
result.removeAll(b);
return result;
}
Java- 共通部分(非破壊で返す)
static <T> List<T> intersection(Collection<T> a, Collection<T> b) {
List<T> result = new ArrayList<>(a);
result.retainAll(b);
return result;
}
Java- 対称差(A △ B)
static <T> List<T> symmetricDifference(Collection<T> a, Collection<T> b) {
List<T> left = new ArrayList<>(a); left.removeAll(b);
List<T> right = new ArrayList<>(b); right.removeAll(a);
left.addAll(right);
return left;
}
Java- Set で高速に(重複無し前提)
Set<String> A = new HashSet<>(List.of("a","b","c"));
Set<String> B = Set.of("b","d");
// 差: A−B
Set<String> diff = new HashSet<>(A);
diff.removeAll(B);
// 積: A∩B
Set<String> inter = new HashSet<>(A);
inter.retainAll(B);
Javaよくある落とし穴と回避策
- 破壊的であることを忘れる:
- 回避: 元を残したいなら必ずコピーしてから removeAll/retainAll。
- equals 実装不備で一致しない:
- 回避: 独自型は equals/hashCode を正しく実装。セット演算に必須。
- List.of / Arrays.asList の変更不可:
- 回避: 変更が必要なら new ArrayList<>(…) に包む。変更不可リストで呼ぶと例外になりうる。
- 重複の扱い(List vs Set):
- 注意: List は重複を保持、removeAll は「一致要素をすべて削除」。重複無視して速くやりたいなら Set 前提で処理。
- 値削除の副作用順序(values.remove)に類似注意:
- ヒント: Map の values で削除すると「どのキーが消えるか未定」のように、差分操作の対象コレクションと副作用範囲を意識する。
まとめ
- removeAll は「A − B」、retainAll は「A ∩ B」。どちらも元のコレクションを変更するため、非破壊で使いたい場合はコピーしてから適用する。独自型は equals/hashCode を整え、重複や性能が気になる場合は Set を選ぶとシンプルかつ高速に差分が取れる。
