1. Обзор
В этом руководстве мы впервые рассмотрим поддержку Lambda в Java 8, в частности, как использовать ее для написания компаратора и сортировки коллекции.
Сначала давайте определим простой класс сущностей:
public class Human {
private String name;
private int age;
// standard constructors, getters/setters, equals and hashcode
}
2. Базовая сортировка без Лямбд
До Java 8 сортировка коллекции включала бы создание анонимного внутреннего класса для компаратора, используемого при сортировке:
new Comparator<Human>() {
@Override
public int compare(Human h1, Human h2) {
return h1.getName().compareTo(h2.getName());
}
}
Это было бы просто использовано для сортировки списка человеческих сущностей:
@Test
public void givenPreLambda_whenSortingEntitiesByName_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 10),
new Human("Jack", 12)
);
Collections.sort(humans, new Comparator<Human>() {
@Override
public int compare(Human h1, Human h2) {
return h1.getName().compareTo(h2.getName());
}
});
Assert.assertThat(humans.get(0), equalTo(new Human("Jack", 12)));
}
3. Базовая сортировка с поддержкой Лямбда
С введением лямбд теперь мы можем обойти анонимный внутренний класс и достичь того же результата с помощью простой функциональной семантики:
(final Human h1, final Human h2) -> h1.getName().compareTo(h2.getName());
Аналогично, теперь мы можем протестировать поведение точно так же, как и раньше:
@Test
public void whenSortingEntitiesByName_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 10),
new Human("Jack", 12)
);
humans.sort(
(Human h1, Human h2) -> h1.getName().compareTo(h2.getName()));
assertThat(humans.get(0), equalTo(new Human("Jack", 12)));
}
Обратите внимание, что мы также используем новый API сортировки, добавленный в java.util.Список в Java 8 вместо старых коллекций.API сортировки.
4. Базовая сортировка Без определения типа
Мы можем еще больше упростить выражение, не указывая определения типов; компилятор способен вывести их самостоятельно:
(h1, h2) -> h1.getName().compareTo(h2.getName())
Опять же, тест остается очень похожим:
@Test
public void
givenLambdaShortForm_whenSortingEntitiesByName_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 10),
new Human("Jack", 12)
);
humans.sort((h1, h2) -> h1.getName().compareTo(h2.getName()));
assertThat(humans.get(0), equalTo(new Human("Jack", 12)));
}
5. Сортировка с использованием ссылки на статический метод
Далее мы собираемся выполнить сортировку, используя лямбда-выражение со ссылкой на статический метод.
Сначала мы собираемся определить метод compareByNameThenAge с точно такой же сигнатурой, что и метод compare в объекте Comparator<Human>:
public static int compareByNameThenAge(Human lhs, Human rhs) {
if (lhs.name.equals(rhs.name)) {
return Integer.compare(lhs.age, rhs.age);
} else {
return lhs.name.compareTo(rhs.name);
}
}
Затем мы собираемся вызвать метод humans.sort с этой ссылкой:
humans.sort(Human::compareByNameThenAge);
Конечным результатом является рабочая сортировка коллекции с использованием статического метода в качестве компаратора:
@Test
public void
givenMethodDefinition_whenSortingEntitiesByNameThenAge_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 10),
new Human("Jack", 12)
);
humans.sort(Human::compareByNameThenAge);
Assert.assertThat(humans.get(0), equalTo(new Human("Jack", 12)));
}
6. Сортировка извлеченных компараторов
Мы также можем избежать определения даже самой логики сравнения, используя ссылку на метод экземпляра и метод Comparator.comparing, который извлекает и создает компаратор на основе этой функции.
Мы собираемся использовать getter getName() для построения лямбда-выражения и сортировки списка по имени:
@Test
public void
givenInstanceMethod_whenSortingEntitiesByName_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 10),
new Human("Jack", 12)
);
Collections.sort(
humans, Comparator.comparing(Human::getName));
assertThat(humans.get(0), equalTo(new Human("Jack", 12)));
}
7. Обратная сортировка
В JDK 8 также введен вспомогательный метод для реверсирования компаратора. Мы можем быстро использовать его для реверсирования нашей сортировки:
@Test
public void whenSortingEntitiesByNameReversed_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 10),
new Human("Jack", 12)
);
Comparator<Human> comparator
= (h1, h2) -> h1.getName().compareTo(h2.getName());
humans.sort(comparator.reversed());
Assert.assertThat(humans.get(0), equalTo(new Human("Sarah", 10)));
}
8. Сортировка С несколькими Условиями
Лямбда-выражения сравнения не обязательно должны быть такими простыми. Мы можем написать и более сложные выражения, например, отсортировать объекты сначала по имени, а затем по возрасту:
@Test
public void whenSortingEntitiesByNameThenAge_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 12),
new Human("Sarah", 10),
new Human("Zack", 12)
);
humans.sort((lhs, rhs) -> {
if (lhs.getName().equals(rhs.getName())) {
return Integer.compare(lhs.getAge(), rhs.getAge());
} else {
return lhs.getName().compareTo(rhs.getName());
}
});
Assert.assertThat(humans.get(0), equalTo(new Human("Sarah", 10)));
}
9. Сортировка с несколькими Условиями – Композиция
Та же логика сравнения, сначала сортировка по имени, а затем по возрасту, также может быть реализована с помощью новой поддержки Comparator composition.
Начиная с JDK 8, теперь мы можем объединять несколько компараторов для построения более сложной логики сравнения:
@Test
public void
givenComposition_whenSortingEntitiesByNameThenAge_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(
new Human("Sarah", 12),
new Human("Sarah", 10),
new Human("Zack", 12)
);
humans.sort(
Comparator.comparing(Human::getName).thenComparing(Human::getAge)
);
Assert.assertThat(humans.get(0), equalTo(new Human("Sarah", 10)));
}
10. Сортировка списка с помощью Stream.sorted()
Мы также можем отсортировать коллекцию, используя API Stream sorted() в Java 8.
Мы можем отсортировать поток, используя естественный порядок, а также порядок, предоставляемый компаратором. Для этого у нас есть два перегруженных варианта API sorted():
- sorted() – сортирует элементы потока, используя естественный порядок; класс element должен реализовывать сопоставимый интерфейс.
- отсортированный(Comparator<? super T> comparator) – сортирует элементы на основе экземпляра Comparator
Давайте рассмотрим пример того, как использовать метод sorted() с естественным упорядочением:
@Test
public final void
givenStreamNaturalOrdering_whenSortingEntitiesByName_thenCorrectlySorted() {
List<String> letters = Lists.newArrayList("B", "A", "C");
List<String> sortedLetters = letters.stream().sorted().collect(Collectors.toList());
assertThat(sortedLetters.get(0), equalTo("A"));
}
Теперь давайте посмотрим, как мы можем использовать пользовательский компаратор с API sorted():
@Test
public final void
givenStreamCustomOrdering_whenSortingEntitiesByName_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12));
Comparator<Human> nameComparator = (h1, h2) -> h1.getName().compareTo(h2.getName());
List<Human> sortedHumans =
humans.stream().sorted(nameComparator).collect(Collectors.toList());
assertThat(sortedHumans.get(0), equalTo(new Human("Jack", 12)));
}
Мы можем еще больше упростить приведенный выше пример, если воспользуемся методом Comparator.comparing():
@Test
public final void
givenStreamComparatorOrdering_whenSortingEntitiesByName_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12));
List<Human> sortedHumans = humans.stream()
.sorted(Comparator.comparing(Human::getName))
.collect(Collectors.toList());
assertThat(sortedHumans.get(0), equalTo(new Human("Jack", 12)));
}
11. Сортировка списка в обратном порядке с помощью Stream.sorted()
Мы также можем использовать Stream.sorted() для сортировки коллекции в обратном порядке.
Сначала давайте посмотрим пример того, как объединить метод sorted() с Comparator.ReverseOrder() для сортировки списка в обратном естественном порядке:
@Test
public final void
givenStreamNaturalOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted() {
List<String> letters = Lists.newArrayList("B", "A", "C");
List<String> reverseSortedLetters = letters.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
assertThat(reverseSortedLetters.get(0), equalTo("C"));
}
Теперь давайте посмотрим, как мы можем использовать метод sorted() и пользовательский компаратор:
@Test
public final void
givenStreamCustomOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12));
Comparator<Human> reverseNameComparator =
(h1, h2) -> h2.getName().compareTo(h1.getName());
List<Human> reverseSortedHumans = humans.stream().sorted(reverseNameComparator)
.collect(Collectors.toList());
assertThat(reverseSortedHumans.get(0), equalTo(new Human("Sarah", 10)));
}
Обратите внимание, что вызов compareTo переворачивается, что отвечает за реверсирование.
Наконец, давайте упростим приведенный выше пример, используя метод Comparator.comparing():
@Test
public final void
givenStreamComparatorOrdering_whenSortingEntitiesByNameReversed_thenCorrectlySorted() {
List<Human> humans = Lists.newArrayList(new Human("Sarah", 10), new Human("Jack", 12));
List<Human> reverseSortedHumans = humans.stream()
.sorted(Comparator.comparing(Human::getName, Comparator.reverseOrder()))
.collect(Collectors.toList());
assertThat(reverseSortedHumans.get(0), equalTo(new Human("Sarah", 10)));
}
12. Нулевые значения
До сих пор мы реализовали наши компараторы таким образом, что они не могут сортировать коллекции, содержащие нулевые значения. То есть, если коллекция содержит хотя бы один нулевой элемент, то метод sort выдает исключение NullPointerException:
@Test(expected = NullPointerException.class)
public void givenANullElement_whenSortingEntitiesByName_thenThrowsNPE() {
List<Human> humans = Lists.newArrayList(null, new Human("Jack", 12));
humans.sort((h1, h2) -> h1.getName().compareTo(h2.getName()));
}
Самое простое решение - обработать нулевые значения вручную в нашей реализации компаратора:
@Test
public void givenANullElement_whenSortingEntitiesByNameManually_thenMovesTheNullToLast() {
List<Human> humans = Lists.newArrayList(null, new Human("Jack", 12), null);
humans.sort((h1, h2) -> {
if (h1 == null) {
return h2 == null ? 0 : 1;
}
else if (h2 == null) {
return -1;
}
return h1.getName().compareTo(h2.getName());
});
Assert.assertNotNull(humans.get(0));
Assert.assertNull(humans.get(1));
Assert.assertNull(humans.get(2));
}
Здесь мы перемещаем все нулевые элементы в конец коллекции. Для этого компаратор считает, что значение null больше ненулевых значений. Когда оба значения равны null, они считаются равными.
Кроме того, мы можем передать любой компаратор, который не является null-безопасным, в метод Comparator.nullsLast() и добиться того же результата:
@Test
public void givenANullElement_whenSortingEntitiesByName_thenMovesTheNullToLast() {
List<Human> humans = Lists.newArrayList(null, new Human("Jack", 12), null);
humans.sort(Comparator.nullsLast(Comparator.comparing(Human::getName)));
Assert.assertNotNull(humans.get(0));
Assert.assertNull(humans.get(1));
Assert.assertNull(humans.get(2));
}
Аналогично, мы можем использовать Comparator.nullsFirst() для перемещения нулевых элементов в начало коллекции:
@Test
public void givenANullElement_whenSortingEntitiesByName_thenMovesTheNullToStart() {
List<Human> humans = Lists.newArrayList(null, new Human("Jack", 12), null);
humans.sort(Comparator.nullsFirst(Comparator.comparing(Human::getName)));
Assert.assertNull(humans.get(0));
Assert.assertNull(humans.get(1));
Assert.assertNotNull(humans.get(2));
}
Настоятельно рекомендуется использовать декораторы nullsFirst() или nullsLast(), поскольку они более гибкие и удобочитаемые.
13. Заключение
В этой статье проиллюстрированы различные и захватывающие способы сортировки списка с использованием лямбда-выражений Java 8, переходя сразу от синтаксического сахара к реальной и мощной функциональной семантике.
Оригинал статьи: https://www.baeldung.com/java-8-sort-lambda