チュートリアル

Java で 2 つのリストを比較する方法(共通・欠けている・等しい要素)

著者 10分で読めます

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 つのリストを比較する方法 をご覧ください。

タグ

#Java#リスト比較#チュートリアル

著者について

songshiqiang個人開発者

AI ツールを使って面白いウェブサイトを開発しています。

関連記事

チュートリアル

Google スプレッドシートで 2 つのリストを比較する方法(一致と差分)

条件付き書式、COUNTIF、MATCH、XLOOKUP、FILTER を使って Google スプレッドシートで 2 つのリストを比較する方法。一致の強調表示、欠けている項目の判定、差分の新しいリストへの抽出、別シートのリストの比較まで解説します。

9分で読めます
#Google スプレッドシート#リスト比較#チュートリアル
続きを読む
チュートリアル

Python で 2 つのリストを比較する方法(サンプルコード付き)

Python で 2 つのリストを比較する完全ガイド。集合で共通・差分の要素を求める方法、リスト内包表記で順序を保つ方法、リストが等しいかの判定、要素ごとの比較、辞書のリストの比較、pandas による大規模データの処理まで解説します。

12分で読めます
#Python#リスト比較#チュートリアル
続きを読む
チュートリアル

Excel で 2 つのリストを比較する方法(一致と差分を見つける)

Excel で 2 つのリストを比較する 9 つの方法。条件付き書式で一致を強調表示し、COUNTIF や MATCH で判定し、VLOOKUP や XLOOKUP で検索し、FILTER で抽出し、行ごとに比較し、大量データは Power Query で処理します。

12分で読めます
#Excel#リスト比較#XLOOKUP#チュートリアル
続きを読む