How to Check if Two Lists are Equal
There are three cases when we are talking about this topic.
Case 1: two lists are strictly equal, they have the same size, the elements in the lists have same order, and all corresponding pairs of elements in the two lists are equal;
Case 2: two lists containing the same elements, they have the same size, and there are duplicate elements within the lists. However, the number of counts each element appears in the two lists must be the same.
Case 3: two lists containing the same elements, regardless of element orders or whether the elements have the same duplication counts.
Let’s discuss one by one.
Case 1: Two lists are Strictly Equal
If we care about element order, we can just use equals() method provided in the Collection class.
List<String> list1 = new ArrayList<>(Arrays.asList("a", "b", "c"));
List<String> list2 = new ArrayList<>(Arrays.asList("a", "b", "c"));
log.info("{}", list1.equals(list2)); // returns true
List<String> list3 = new ArrayList<>(Arrays.asList("a", "b", "a"));
List<String> list4 = new ArrayList<>(Arrays.asList("a", "a", "b"));
log.info("{}", list3.equals(list4)); // returns false because of dismatch orders
The equals() method also works on custom objects. Suppose we have an UserIdCard class with three fields: id, name, age. We see two UserIdCard objects are the same if they have the same id value, regardless of the other two fields.
We can reduce lots of work to create UserIdCard class with help of Lombok. By using @EqualsAndHashCode(onlyExplicitlyIncluded = true) on class and @EqualsAndHashCode.Include annotation on id fields, id filed is included as the only checking field in the equals() and hashcode() method implementations.
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class UserIdCard {
@EqualsAndHashCode.Include
private Integer id;
private String name;
private Integer age;
}
As the below codes shown, list5 is equals to list6 because the id of corresponding user in both lists are equal.
elements in list7 has different order with elements in list8, so list7.equals(list8) returns false in the end.
List<UserIdCard> list5 = new ArrayList<>(Arrays.asList(
new UserIdCard(1, "tom", 30),
new UserIdCard(2, "jack", 25),
new UserIdCard(3, "joey", 30)
));
List<UserIdCard> list6 = new ArrayList<>(Arrays.asList(
new UserIdCard(1, "tom", 20),
new UserIdCard(2, "jack", 20),
new UserIdCard(3, "joey", 20)
));
log.info("{}", list5.equals(list6)); // returns true
List<UserIdCard> list7 = new ArrayList<>(Arrays.asList(
new UserIdCard(1, "tom", 30),
new UserIdCard(2, "jack", 25),
new UserIdCard(3, "joey", 30)
));
List<UserIdCard> list8 = new ArrayList<>(Arrays.asList(
new UserIdCard(1, "tom", 30),
new UserIdCard(2, "jack", 25),
new UserIdCard(4, "joey", 30)
));
log.info("{}", list7.equals(list8)); // returns false
An alternative to Collection.equals() is Objects.equals() from java.util package. Below is the implementation of Objects.equals():
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
From the souce code, we can know that Objects.equals() internally calls equals() method of object. The advantage of using this method is that there’s no need to explicitly handle nulls because Objects.equals() helps doing this.
When we reuse the declaration of list1 to list8 and check using Objects.equals(), we get the same result as when we use Collection.equals().
log.info("list1 is equals to list2: {}", Objects.equals(list1, list2)); // returns true
log.info("list3 is equals to list4: {}", Objects.equals(list3, list4)); // returns false
log.info("list5 is equals to list6: {}", Objects.equals(list5, list6)); // returns true
log.info("list7 is equals to list8: {}", Objects.equals(list7, list8)); // returns false
Case 2: Two Lists Containing the Same Elements with the Same Duplicate Counts
Let’s understand this case by several examples directly.
list1with elements (“a”, “b”, “b”) is equal tolist2with elements (“b”, “b”, “a”) because they both contain “a” and “b”, and there are one “a” and two “b” in both list.list3with elements (“a”, “a”) is not equal tolist4with elements (“a”, “a”, “a”) because even though both lists contain “a”, they have different sizes, andlist3has two “a” butlist4has three “a”.
CollectionUtils.isEqualCollection() method from apache.commons.collections4 is the best fit to solve this case.
List<String> list1 = new ArrayList<>(Arrays.asList("a", "b", "b"));
List<String> list2 = new ArrayList<>(Arrays.asList("b", "b", "a"));
log.info("{}", CollectionUtils.isEqualCollection(list1, list2)); // returns true
List<String> list3 = new ArrayList<>(Arrays.asList("a", "a"));
List<String> list4 = new ArrayList<>(Arrays.asList("a", "a", "a"));
log.info("{}", CollectionUtils.isEqualCollection(list3, list4)); // returns false
If we are not allowed to have apache.commons.collections4 in our project, we have to write our own version of isEqualCollection(), let’s get idea from the source code of isEqualCollection():
public static boolean isEqualCollection(final Collection<?> a, final Collection<?> b) {
if(a.size() != b.size()) {
return false;
}
final CardinalityHelper<Object> helper = new CardinalityHelper<>(a, b);
if(helper.cardinalityA.size() != helper.cardinalityB.size()) {
return false;
}
for( final Object obj : helper.cardinalityA.keySet()) {
if(helper.freqA(obj) != helper.freqB(obj)) {
return false;
}
}
return true;
}
The key idea of isEqualCollection() is to convert both lists to maps. The key of map is the elements in the lists, and the value of the map is the elements occurrences in the lists. By doing this, we can check key set of maps and see if two lists containing the same value, and we can know if element occurrences are equals in the two lists.
Our own version of isEqualCollection() could be like this:
private static boolean isEqualsCollection(List<String> a, List<String> b) {
if(a.size() != b.size()) {
return false;
}
Map<String, Integer> aOccuranceMap = getOccurrenceMap(a);
Map<String, Integer> bOccuranceMap = getOccurrenceMap(b);
// check if two lists containing the same elements
if(aOccuranceMap.size() != bOccuranceMap.size()) {
return false;
}
// check if element occurrences are equals in the two lists
for( final String element : aOccuranceMap.keySet()) {
if(!aOccuranceMap.get(element).equals(bOccuranceMap.get(element))) {
return false;
}
}
return true;
}
public static Map<String, Integer> getOccurrenceMap(final List<String> a) {
final Map<String, Integer> count = new HashMap<>();
for (final String element : a) {
count.merge(element, 1, Integer::sum);
}
return count;
}
Case 3: Two Lists Containing the Same Elements, Regardless of Orders or Dupliates
In most cases, we just want to know if two lists containing the same elements, it is acceptable that elements have different duplicate counts in the two lists. We can simply use List.containsAll() method to achieve this goal.
List<String> list1 = new ArrayList<>(Arrays.asList("a", "b", "b"));
List<String> list2 = new ArrayList<>(Arrays.asList("b", "b"));
log.info("{}", list1.containsAll(list2) && list2.containsAll(list1)); // returns false
List<String> list3 = new ArrayList<>(Arrays.asList("a", "a", "a"));
List<String> list4 = new ArrayList<>(Arrays.asList("a"));
log.info("{}", list3.containsAll(list4) && list4.containsAll(list3)); // returns true
We may also convert list to set and use Set.containsAll() instead when there are lots of duplicates in the lists.
Set<String> set1 = new HashSet<>(list1);
Set<String> set2 = new HashSet<>(list2);
log.info("{}", set1.containsAll(set2) && set2.containsAll(set1)); // returns false