Правильный способ утверждения двух равных списков, игнорируя порядок

Научитесь сравнивать два списка в Java так, чтобы оба списка содержали абсолютно одинаковые элементы в любом порядке, а вхождения каждого элемента списка были одинаковыми в обоих списках.

1. Использование CollectionUtils.isEqualCollection() из Common Collections4

Чтобы использовать этот API, включите последнюю версию commons-collections4 из репозитория Maven.

<dependency><groupId>org.apache.commons</groupId><artifactId>commons-collections4</artifactId><version>4.4</version></dependency>

API isEqualCollection() возвращает значение true, если оба списка содержат абсолютно одинаковые элементы с абсолютно одинаковой мощностью(т. е. элемент присутствует одинаковое количество раз в обоих списках).

List<String> list = Arrays.asList("a", "b", "c");List<String> equalList = Arrays.asList("b", "c", "a");List<String> unequalList = Arrays.asList("d", "c", "a");assertTrue(CollectionUtils.isEqualCollection(list, equalList));assertFalse(CollectionUtils.isEqualCollection(list, unequalList)); 

Мы можем убедиться, что при сравнении списков учитывается также мощность.

assertFalse(CollectionUtils.isEqualCollection(Arrays.asList("a", "a", "b"),Arrays.asList("a", "b", "b")));assertTrue(CollectionUtils.isEqualCollection(Arrays.asList("a", "b", "b"),Arrays.asList("a", "b", "b")));

По умолчанию элементы списка сравниваются с помощью метода equals(). Иногда мы получаем списки из разных источников, и элементы списка могут не содержать все поля, необходимые для правил равенства по умолчанию. Мы также можем предоставить пользовательскую логику равенства, предоставив пользовательскую реализацию интерфейса Equator.

В следующем примере мы сравниваем экземпляры String, игнорируя ВЕРХНИЙ или нижний регистр. Обратите внимание, что мощности проверяются с использованием экземпляра Map, поэтому необходимо, чтобы одни и те же элементы были преобразованы в одни и те же ключи. В этом случае все ключи будут преобразованы в строки ВЕРХНЕГО РЕГИСТРА.

class StringCaseEquator implements Equator<String> {public boolean equate(String s1, String s2) {return s1.equalsIgnoreCase(s2);}@Overridepublic int hash(String s) {return s.toUpperCase().hashCode();}}assertTrue(CollectionUtils.isEqualCollection(Arrays.asList("a", "b"),Arrays.asList("A", "B"),new StringCaseEquator()));

2. Использование Matchers.containsInAnyOrder() от Hamcrest.

Чтобы использовать этот API, включите последнюю версию hamcrest-all из репозитория Maven.

<dependency><groupId>org.hamcrest</groupId><artifactId>hamcrest-all</artifactId><version>1.3</version></dependency>

API containsInAnyOrder() создает независимый от порядка сопоставитель для Iterables, который сопоставляет элементы двух списков, игнорируя порядок элементов в списке. Для положительного совпадения оба списка должны быть одинакового размера, поэтому нам не нужно явно сравнивать размеры.

List<String> list = Arrays.asList("a", "b", "c");List<String> equalList = Arrays.asList("b", "c", "a");assertThat(list, Matchers.containsInAnyOrder(equalList.toArray()));

Это решение не поддерживает пользовательскую логику равенства для элементов списка.

3. Использование API AssertJ containsExactlyInAnyOrderElementsOf()

Начните с добавления последней версии assertj-core из репозитория Maven.

<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.16.1</version></dependency>

Функция containsExactlyInAnyOrderElementsOf() проверяет, что первый список содержит все элементы второго списка, и каждый элемент встречается одинаковое количество раз в любом порядке.

List<String> list = Arrays.asList("a", "b", "c");List<String> equalList = Arrays.asList("b", "c", "a");org.assertj.core.api.Assertions.assertThat(list).containsExactlyInAnyOrderElementsOf(equalList);

Любое дополнительное вхождение любого элемента в список приведет к невыполнению утверждения.

4. Обычный Java или JUnit

Если мы не хотим использовать никакую стороннюю библиотеку, мы можем разработать наше решение с помощью метода List.remove(). В следующем коде мы перебираем первый список и удаляем первое вхождение текущего элемента из второго списка.

После завершения итерации второй список должен быть пустым. Обратите внимание, что этот подход использует API remove(), доступный в классе ArrayList. Поэтому, если вы используете неизменяемый список, рассмотрите возможность обернуть его в экземпляры ArrayList.

static boolean compareListsIgnoringOrder(ArrayList list1, ArrayList list2) {if(list1 == null || list2 == null) return false;if(list1.size() != list2.size()) return false;for(Object o : list1) {list2.remove(o);}if(list2.size() != 0) return false;return true;}List<String> firstList = Arrays.asList("a", "b", "c", "c");List<String> secondList = Arrays.asList("b", "c", "a", "c");assertTrue(compareListsIgnoringOrder(new ArrayList(firstList), new ArrayList<>(secondList)));

5. Заключение

Сравнение двух списков Java с игнорированием порядка является обычным требованием во время модульных тестов, когда оба списка поступают из разных источников, и мы должны проверить, что оба списка имеют одинаковые элементы независимо от их порядка в списке. В этом руководстве показаны примеры с использованием Apache Common Collections4, Hamcrest, AssertJ и простых API Java для использования в операторах утверждений JUnit.

Исходный код на Github

Прокрутить вверх