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.

  • list1 with elements (“a”, “b”, “b”) is equal to list2 with elements (“b”, “b”, “a”) because they both contain “a” and “b”, and there are one “a” and two “b” in both list.
  • list3 with elements (“a”, “a”) is not equal to list4 with elements (“a”, “a”, “a”) because even though both lists contain “a”, they have different sizes, and list3 has two “a” but list4 has 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