How to Compare Two Lists in Java (Common, Missing and Equal Elements)
Comparing two lists comes up all the time in Java: syncing database records, checking API responses in tests, or finding which IDs were added or removed between two exports. Depending on the question, you need a different tool:
- Are the lists equal? – with or without caring about order and duplicates
- Which elements are in both lists? – the intersection
- Which elements are only in one list? – the difference
This guide covers each case with the standard library (Java 16+ for Stream.toList() and records), plus Apache Commons and Guava alternatives. All snippets use these imports:
import java.util.*;
import java.util.stream.*;
Only need to compare two lists once? Paste them into the free Compare Two Lists tool to see what's only in A, only in B and in both – no code required.
1. Check whether two lists are equal
List.equals() returns true only when both lists contain the same elements in the same order:
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
It compares elements with their own equals() method, so it works for strings, numbers and any class that implements equals() properly. It also works across implementations: an ArrayList and a LinkedList with the same elements are equal.
2. Compare lists ignoring order
How you ignore order depends on whether duplicates matter.
Same elements and same counts (duplicates matter) – sort copies and compare them:
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
If the elements can't be sorted, compare frequency maps instead:
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
Same distinct elements (duplicates ignored) – compare sets:
List<Integer> z = List.of(1, 2, 3);
System.out.println(new HashSet<>(x).equals(new HashSet<>(z))); // true
3. Find common elements (intersection) with retainAll
retainAll() removes every element that is not in the other collection. It modifies the list it's called on, so work on a copy:
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]
Copying into a new ArrayList matters for a second reason: lists created with List.of() or Arrays.asList() don't support removal, and calling retainAll() on them throws UnsupportedOperationException.
4. Find missing elements (difference) with removeAll
removeAll() is the opposite: it removes everything that is in the other collection, leaving the elements unique to the first list:
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]
Both methods keep the original order of the list you start from, and they keep duplicates of elements that survive.
5. The same with streams
Streams don't modify the source lists and read nicely when you need extra filtering:
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]
To get the elements that are in exactly one of the lists (the symmetric difference), combine both directions:
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]
For a case-insensitive comparison, normalize before building the set – for example listB.stream().map(String::toLowerCase).collect(Collectors.toSet()) – and lowercase each item you check against it.
6. Compare lists of objects
Collections compare objects with equals() and, for hash-based collections, hashCode(). Java records generate both from their fields, so they work out of the box:
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]]
For a regular class, override both equals() and hashCode() – if you only override equals(), HashSet lookups won't find matching objects.
Often you want to compare objects by a key instead of all fields, so that a changed name doesn't count as a different customer:
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. Library helpers: Apache Commons and Guava
If your project already uses one of these libraries, they offer one-line set operations.
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) works on sets and returns live views:
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 respects how many times each element occurs; Guava's Sets work on distinct elements. Copy a Guava view with new HashSet<>(view) if the underlying sets will change.
8. Performance: don't search a List inside a loop
list.contains() checks elements one by one, so ArrayList.removeAll(otherList) and filter(otherList::contains) compare every element of one list with every element of the other. For two lists of 100,000 items that can mean up to 10 billion comparisons.
Wrapping the other list in a HashSet first – as all the examples above do – turns each lookup into a constant-time operation, so the whole comparison takes roughly 100,000 steps:
// 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();
Summary
| Goal | Recommended approach | Keeps order? | Notes |
|---|---|---|---|
| Equal, same order | a.equals(b) |
– | Works across List implementations |
| Equal, any order, with duplicates | Sort copies or compare frequency maps | – | Maps work for unsortable elements |
| Equal, any order, no duplicates | new HashSet<>(a).equals(new HashSet<>(b)) |
– | Ignores repeated elements |
| Common elements | copy.retainAll(new HashSet<>(b)) |
Yes | Copy first; List.of is immutable |
| Only in A | copy.removeAll(new HashSet<>(b)) or stream filter |
Yes | Stream version doesn't modify anything |
| In exactly one list | Stream.concat of both differences |
Yes | Or CollectionUtils.disjunction |
| Lists of objects | Records, or equals() + hashCode() |
Yes | Or compare by a key such as id |
Start with a HashSet lookup and a stream filter – it's fast, doesn't modify your lists, and handles almost every case. For quick one-off checks outside your code, the online list comparison tool does the same in your browser. Working in Python instead? See How to Compare Two Lists in Python.