Java で 2 つのリストを比較する方法(共通・欠けている・等しい要素)
Java では 2 つのリストを比較する場面がよくあります。データベースのレコードの同期、テストでの API レスポンスの確認、2 回のエクスポートの間に追加・削除された ID の洗い出しなどです。知りたいことによって、使う方法が変わります。
- リストは等しいか? ― 順序や重複を考慮するかどうか
- 両方のリストにある要素は? ― 積集合
- 片方のリストにしかない要素は? ― 差集合
このガイドでは、標準ライブラリ(Stream.toList() と record は Java 16 以降)でそれぞれのケースを解説し、Apache Commons と Guava による代替方法も紹介します。コード例ではすべて次のインポートを使います。
import java.util.*;
import java.util.stream.*;
2 つのリストを一度比較したいだけなら、無料の 2 つのリスト比較ツール に貼り付ければ、コードなしで A のみ・B のみ・両方にある項目がわかります。
1. 2 つのリストが等しいかを判定する
List.equals() は、両方のリストが同じ要素を 同じ順序で 含む場合にだけ true を返します。
List<Integer> a = List.of(1, 2, 3);
List<Integer> b = List.of(1, 2, 3);
List<Integer> c = List.of(3, 2, 1);
System.out.println(a.equals(b)); // true
System.out.println(a.equals(c)); // false – same elements, different order
要素はそれぞれの equals() メソッドで比較されるため、文字列や数値、equals() を正しく実装したクラスで使えます。実装クラスが違っても動作し、同じ要素を持つ ArrayList と LinkedList は等しいと判定されます。
2. 順序を無視してリストを比較する
順序の無視の仕方は、重複に意味があるかどうかで変わります。
同じ要素が同じ回数ずつ(重複に意味がある) ― コピーを並べ替えて比較します。
List<Integer> x = Arrays.asList(3, 1, 2, 2);
List<Integer> y = Arrays.asList(2, 3, 2, 1);
List<Integer> sortedX = new ArrayList<>(x);
List<Integer> sortedY = new ArrayList<>(y);
Collections.sort(sortedX);
Collections.sort(sortedY);
System.out.println(sortedX.equals(sortedY)); // true
要素を並べ替えられない場合は、出現回数のマップを比較します。
Map<Integer, Long> countX = x.stream()
.collect(Collectors.groupingBy(i -> i, Collectors.counting()));
Map<Integer, Long> countY = y.stream()
.collect(Collectors.groupingBy(i -> i, Collectors.counting()));
System.out.println(countX.equals(countY)); // true
含まれる要素の種類が同じ(重複は無視) ― Set で比較します。
List<Integer> z = List.of(1, 2, 3);
System.out.println(new HashSet<>(x).equals(new HashSet<>(z))); // true
3. retainAll で共通の要素(積集合)を求める
retainAll() は、もう一方のコレクションに ない 要素をすべて削除します。呼び出したリスト自体を変更するので、コピーに対して実行しましょう。
List<String> listA = List.of("apple", "banana", "cherry", "date");
List<String> listB = List.of("banana", "date", "fig");
List<String> common = new ArrayList<>(listA);
common.retainAll(new HashSet<>(listB));
System.out.println(common); // [banana, date]
新しい ArrayList にコピーするのにはもう 1 つ理由があります。List.of() や Arrays.asList() で作ったリストは削除をサポートしておらず、retainAll() を呼ぶと UnsupportedOperationException が発生します。
4. removeAll で欠けている要素(差集合)を求める
removeAll() はその逆で、もう一方のコレクションに ある 要素をすべて削除し、最初のリストにしかない要素を残します。
List<String> onlyInA = new ArrayList<>(listA);
onlyInA.removeAll(new HashSet<>(listB));
System.out.println(onlyInA); // [apple, cherry]
List<String> onlyInB = new ArrayList<>(listB);
onlyInB.removeAll(new HashSet<>(listA));
System.out.println(onlyInB); // [fig]
どちらのメソッドも元のリストの順序を保ち、残った要素の重複もそのまま残します。
5. Stream を使う
Stream は元のリストを変更せず、追加の条件で絞り込む場合も読みやすく書けます。
Set<String> setB = new HashSet<>(listB);
List<String> onlyInA = listA.stream()
.filter(item -> !setB.contains(item))
.toList(); // Java 16+; use collect(Collectors.toList()) before that
List<String> inBoth = listA.stream()
.filter(setB::contains)
.toList();
System.out.println(onlyInA); // [apple, cherry]
System.out.println(inBoth); // [banana, date]
どちらか 一方だけ にある要素(対称差)を求めるには、両方向の差分を結合します。
Set<String> setA = new HashSet<>(listA);
List<String> inOnlyOne = Stream.concat(
listA.stream().filter(item -> !setB.contains(item)),
listB.stream().filter(item -> !setA.contains(item)))
.toList();
System.out.println(inOnlyOne); // [apple, cherry, fig]
大文字小文字を区別せずに比較するには、Set を作る前に値を正規化し(例:listB.stream().map(String::toLowerCase).collect(Collectors.toSet()))、照合する側の要素も小文字にします。
6. オブジェクトのリストを比較する
コレクションはオブジェクトを equals() で比較し、ハッシュベースのコレクションでは hashCode() も使います。Java の record はフィールドからこの両方を自動生成するので、そのまま使えます。
record Customer(int id, String name) {}
List<Customer> oldCustomers = List.of(new Customer(1, "Ann"), new Customer(2, "Bob"));
List<Customer> newCustomers = List.of(new Customer(2, "Bob"), new Customer(3, "Cy"));
Set<Customer> newSet = new HashSet<>(newCustomers);
List<Customer> removed = oldCustomers.stream()
.filter(c -> !newSet.contains(c))
.toList();
System.out.println(removed); // [Customer[id=1, name=Ann]]
通常のクラスでは、equals() と hashCode() の 両方 をオーバーライドしてください。equals() だけをオーバーライドすると、HashSet で一致するオブジェクトが見つかりません。
名前が変わっても別の顧客として扱われないよう、すべてのフィールドではなくキーで比較したい場合もよくあります。
Set<Integer> newIds = newCustomers.stream()
.map(Customer::id)
.collect(Collectors.toSet());
List<Customer> removedById = oldCustomers.stream()
.filter(c -> !newIds.contains(c.id()))
.toList();
System.out.println(removedById); // [Customer[id=1, name=Ann]]
7. ライブラリ:Apache Commons と Guava
プロジェクトですでにこれらのライブラリを使っているなら、集合演算を 1 行で書けます。
Apache Commons Collections(org.apache.commons.collections4.CollectionUtils):
Collection<String> onlyInA = CollectionUtils.subtract(listA, listB); // [apple, cherry]
Collection<String> inBoth = CollectionUtils.intersection(listA, listB); // banana, date (any order)
Collection<String> inOnlyOne = CollectionUtils.disjunction(listA, listB); // apple, cherry, fig (any order)
Guava(com.google.common.collect.Sets)は Set を対象とし、ライブビューを返します。
Set<String> setA = new HashSet<>(listA);
Set<String> setB = new HashSet<>(listB);
Set<String> onlyInA = Sets.difference(setA, setB);
Set<String> inBoth = Sets.intersection(setA, setB);
Set<String> inOnlyOne = Sets.symmetricDifference(setA, setB);
CollectionUtils は各要素の出現回数を考慮しますが、Guava の Sets は異なる要素だけを扱います。元の Set が後から変わる場合は、new HashSet<>(view) で Guava のビューをコピーしてください。
8. パフォーマンス:ループの中で List を検索しない
list.contains() は要素を 1 つずつ確認するため、ArrayList.removeAll(otherList) や filter(otherList::contains) は、一方のリストのすべての要素をもう一方のすべての要素と比較します。10 万件のリスト同士なら、最大で 100 億回の比較になります。
上のすべての例のように、もう一方のリストを先に HashSet に入れておけば、各検索は定数時間になり、比較全体もおよそ 10 万ステップで終わります。
// Slow: O(n × m)
listA.stream().filter(listB::contains).toList();
// Fast: O(n + m)
Set<String> lookup = new HashSet<>(listB);
listA.stream().filter(lookup::contains).toList();
まとめ
| 目的 | おすすめの方法 | 順序を保つ? | 補足 |
|---|---|---|---|
| 等しい(同じ順序) | a.equals(b) |
– | List の実装が違っても使える |
| 等しい(順序不問・重複あり) | コピーを並べ替える、または出現回数のマップを比較 | – | マップは並べ替えできない要素にも使える |
| 等しい(順序不問・重複なし) | new HashSet<>(a).equals(new HashSet<>(b)) |
– | 重複は無視される |
| 共通の要素 | copy.retainAll(new HashSet<>(b)) |
はい | 先にコピー。List.of は変更不可 |
| A のみ | copy.removeAll(new HashSet<>(b)) または Stream のフィルタ |
はい | Stream 版は何も変更しない |
| 一方だけにある要素 | 両方向の差分を Stream.concat |
はい | または CollectionUtils.disjunction |
| オブジェクトのリスト | record、または equals() + hashCode() |
はい | id などのキーで比較する方法も |
まずは HashSet による検索と Stream のフィルタから始めましょう。高速で、リストを変更せず、ほとんどのケースに対応できます。コードの外でさっと確認したいときは、オンラインのリスト比較ツール がブラウザ上で同じことを行います。Python をお使いの場合は Python で 2 つのリストを比較する方法 をご覧ください。