Перейти к основному содержимому

Руководство по Java 8 Comparator.comparing()

· 8 мин. чтения

1. Обзор

Java 8 представила несколько улучшений интерфейса Comparator , в том числе несколько статических функций, которые очень полезны при определении порядка сортировки коллекций.

Интерфейс Comparator также может эффективно использовать лямбда-выражения Java 8. Подробное объяснение лямбда-выражений и компаратора можно найти здесь , а хронику применения компаратора и сортировки можно найти здесь .

В этом руководстве мы рассмотрим несколько функций, представленных для интерфейса Comparator в Java 8 .

2. Начало работы

2.1. Пример класса компонента

Для примеров в этом руководстве давайте создадим bean-компонент Employee и будем использовать его поля для целей сравнения и сортировки:

public class Employee {
String name;
int age;
double salary;
long mobile;

// constructors, getters & setters
}

2.2. Наши данные тестирования

Мы также создадим массив сотрудников, которые будем использовать для хранения результатов нашего типа в различных тестовых примерах на протяжении всего руководства:

employees = new Employee[] { ... };

Начальный порядок элементов сотрудников будет:

[Employee(name=John, age=25, salary=3000.0, mobile=9922001), 
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

На протяжении всего руководства мы будем сортировать приведенный выше массив сотрудников , используя различные функции.

Для тестовых утверждений мы будем использовать набор предварительно отсортированных массивов, которые мы будем сравнивать с нашими результатами сортировки (т . е. с массивом сотрудников ) для различных сценариев.

Давайте объявим несколько таких массивов:

@Before
public void initData() {
sortedEmployeesByName = new Employee[] {...};
sortedEmployeesByNameDesc = new Employee[] {...};
sortedEmployeesByAge = new Employee[] {...};

// ...
}

Как всегда, не стесняйтесь обращаться к нашей ссылке GitHub для получения полного кода.

3. Использование Comparator.comparing

В этом разделе мы рассмотрим варианты статической функции Comparator.comparing .

3.1. Вариант выбора ключа

Статическая функция Comparator.comparing принимает функцию ключа сортировки и возвращает Comparator для типа, содержащего ключ сортировки:

static <T,U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T,? extends U> keyExtractor)

Чтобы увидеть это в действии, мы будем использовать поле имени в Employee в качестве ключа сортировки и передавать ссылку на его метод в качестве аргумента типа Function. Компаратор , возвращенный из того же, используется для сортировки:

@Test
public void whenComparing_thenSortedByName() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);

Arrays.sort(employees, employeeNameComparator);

assertTrue(Arrays.equals(employees, sortedEmployeesByName));
}

В результате сортировки значения массива сотрудников упорядочены по именам:

[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

3.2. Ключевой селектор и вариант компаратора

Есть еще один вариант, который облегчает переопределение естественного порядка ключа сортировки, предоставляя компаратор , который создает пользовательский порядок для ключа сортировки:

static <T,U> Comparator<T> comparing(
Function<? super T,? extends U> keyExtractor,
Comparator<? super U> keyComparator)

Итак, давайте изменим тест выше. Мы переопределим естественный порядок сортировки по полю имени , предоставив Comparator для сортировки имен в порядке убывания в качестве второго аргумента Comparator.comparing :

@Test
public void whenComparingWithComparator_thenSortedByNameDesc() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(
Employee::getName, (s1, s2) -> {
return s2.compareTo(s1);
});

Arrays.sort(employees, employeeNameComparator);

assertTrue(Arrays.equals(employees, sortedEmployeesByNameDesc));
}

Как мы видим, результаты отсортированы в порядке убывания имени :

[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001)]

3.3. Использование Comparator.reversed

При вызове существующего Comparator метод экземпляра Comparator.reversed возвращает новый Comparator , который меняет порядок сортировки оригинала.

Мы будем использовать компаратор , который сортирует сотрудников по именам и реверсирует его так, чтобы сотрудники сортировались в порядке убывания имени :

@Test
public void whenReversed_thenSortedByNameDesc() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);
Comparator<Employee> employeeNameComparatorReversed
= employeeNameComparator.reversed();
Arrays.sort(employees, employeeNameComparatorReversed);
assertTrue(Arrays.equals(employees, sortedEmployeesByNameDesc));
}

Теперь результаты отсортированы в порядке убывания имени :

[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001)]

3.4. Использование Comparator.comparingInt

Также есть функция Comparator.comparingInt, которая делает то же самое, что и Comparator.comparing , но принимает только селекторы int . Давайте попробуем это на примере, где мы упорядочиваем сотрудников по возрасту :

@Test
public void whenComparingInt_thenSortedByAge() {
Comparator<Employee> employeeAgeComparator
= Comparator.comparingInt(Employee::getAge);

Arrays.sort(employees, employeeAgeComparator);

assertTrue(Arrays.equals(employees, sortedEmployeesByAge));
}

После сортировки значения массива сотрудников имеют следующий порядок:

[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

3.5. Использование Comparator.comparingLong

Подобно тому, что мы сделали для ключей int , давайте рассмотрим пример использования Comparator.comparingLong для рассмотрения ключа сортировки типа long путем упорядочения массива сотрудников по мобильному полю:

@Test
public void whenComparingLong_thenSortedByMobile() {
Comparator<Employee> employeeMobileComparator
= Comparator.comparingLong(Employee::getMobile);

Arrays.sort(employees, employeeMobileComparator);

assertTrue(Arrays.equals(employees, sortedEmployeesByMobile));
}

После сортировки значения массива сотрудников имеют следующий порядок с мобильным телефоном в качестве ключа:

[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401), 
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001)]

3.6. Использование Comparator.comparingDouble

Опять же, как и для ключей int и long , давайте рассмотрим пример использования Comparator.comparingDouble для рассмотрения ключа сортировки типа double , упорядочивая массив сотрудников по полю зарплаты :

@Test
public void whenComparingDouble_thenSortedBySalary() {
Comparator<Employee> employeeSalaryComparator
= Comparator.comparingDouble(Employee::getSalary);

Arrays.sort(employees, employeeSalaryComparator);

assertTrue(Arrays.equals(employees, sortedEmployeesBySalary));
}

После сортировки значения массива сотрудников имеют следующий порядок с зарплатой в качестве ключа сортировки:

[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

4. Учет естественного порядка в компараторе

Мы можем определить естественный порядок по поведению реализации интерфейса Comparable . Более подробную информацию о различиях между Comparator и использованием интерфейса Comparable можно найти в этой статье .

Давайте реализуем Comparable в нашем классе Employee , чтобы мы могли попробовать функции naturalOrder и reverseOrder интерфейса Comparator :

public class Employee implements Comparable<Employee>{
// ...

@Override
public int compareTo(Employee argEmployee) {
return name.compareTo(argEmployee.getName());
}
}

4.1. Использование естественного порядка

Функция naturalOrder возвращает Comparator для возвращаемого типа, указанного в подписи:

static <T extends Comparable<? super T>> Comparator<T> naturalOrder()

Учитывая приведенную выше логику для сравнения сотрудников на основе поля имени , давайте используем эту функцию для получения компаратора , который сортирует массив сотрудников в естественном порядке:

@Test
public void whenNaturalOrder_thenSortedByName() {
Comparator<Employee> employeeNameComparator
= Comparator.<Employee> naturalOrder();

Arrays.sort(employees, employeeNameComparator);

assertTrue(Arrays.equals(employees, sortedEmployeesByName));
}

После сортировки значения массива сотрудников имеют следующий порядок:

[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

4.2. Использование обратного естественного порядка

Подобно тому, как мы использовали naturalOrder , мы будем использовать метод reverseOrder для создания компаратора , который будет производить обратный порядок сотрудников по сравнению с тем, что в примере naturalOrder :

@Test
public void whenReverseOrder_thenSortedByNameDesc() {
Comparator<Employee> employeeNameComparator
= Comparator.<Employee> reverseOrder();

Arrays.sort(employees, employeeNameComparator);

assertTrue(Arrays.equals(employees, sortedEmployeesByNameDesc));
}

После сортировки значения массива сотрудников имеют следующий порядок:

[Employee(name=Keith, age=35, salary=4000.0, mobile=3924401), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001)]

5. Учет нулевых значений в компараторе

В этом разделе мы рассмотрим функции nullsFirst и nullsLast , которые учитывают нулевые значения при упорядочении и сохраняют нулевые значения в начале или в конце упорядоченной последовательности.

5.1. С учетом нуля в первую очередь

Давайте случайным образом вставим нулевые значения в массив сотрудников :

[Employee(name=John, age=25, salary=3000.0, mobile=9922001), 
null,
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
null,
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

Функция nullsFirst вернет компаратор , который сохраняет все нули в начале последовательности упорядочивания:

@Test
public void whenNullsFirst_thenSortedByNameWithNullsFirst() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);
Comparator<Employee> employeeNameComparator_nullFirst
= Comparator.nullsFirst(employeeNameComparator);

Arrays.sort(employeesArrayWithNulls,
employeeNameComparator_nullFirst);

assertTrue(Arrays.equals(
employeesArrayWithNulls,
sortedEmployeesArray_WithNullsFirst));
}

После сортировки значения массива сотрудников имеют следующий порядок:

[null, 
null,
Employee(name=Ace, age=22, salary=2000.0, mobile=5924001),
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

5.2. Учитывая нулевой последний

Функция nullsLast вернет компаратор , который сохраняет все нули в конце последовательности упорядочения:

@Test
public void whenNullsLast_thenSortedByNameWithNullsLast() {
Comparator<Employee> employeeNameComparator
= Comparator.comparing(Employee::getName);
Comparator<Employee> employeeNameComparator_nullLast
= Comparator.nullsLast(employeeNameComparator);

Arrays.sort(employeesArrayWithNulls, employeeNameComparator_nullLast);

assertTrue(Arrays.equals(
employeesArrayWithNulls, sortedEmployeesArray_WithNullsLast));
}

После сортировки значения массива сотрудников имеют следующий порядок:

[Employee(name=Ace, age=22, salary=2000.0, mobile=5924001), 
Employee(name=John, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401),
null,
null]

6. Использование Comparator.thenComparing

Функция thenComparing позволяет нам настроить лексикографическое упорядочение значений, предоставив несколько ключей сортировки в определенной последовательности.

Давайте посмотрим на другой массив класса Employee :

someMoreEmployees = new Employee[] { ... };

Мы рассмотрим следующую последовательность элементов в приведенном выше массиве:

[Employee(name=Jake, age=25, salary=3000.0, mobile=9922001), 
Employee(name=Jake, age=22, salary=2000.0, mobile=5924001),
Employee(name=Ace, age=22, salary=3000.0, mobile=6423001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

Затем мы напишем последовательность сравнений в виде возраста , за которым следует имя, и посмотрим порядок этого массива:

@Test
public void whenThenComparing_thenSortedByAgeName(){
Comparator<Employee> employee_Age_Name_Comparator
= Comparator.comparing(Employee::getAge)
.thenComparing(Employee::getName);

Arrays.sort(someMoreEmployees, employee_Age_Name_Comparator);

assertTrue(Arrays.equals(someMoreEmployees, sortedEmployeesByAgeName));
}

Здесь порядок будет выполняться по возрасту , а для значений с одинаковым возрастом порядок будет выполняться по имени . Мы можем видеть это в последовательности, которую мы получаем после сортировки:

[Employee(name=Ace, age=22, salary=3000.0, mobile=6423001), 
Employee(name=Jake, age=22, salary=2000.0, mobile=5924001),
Employee(name=Jake, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

Теперь мы можем использовать другую версию thenComparing , thenComparingInt , изменив лексикографическую последовательность на имя , за которым следует возраст :

@Test
public void whenThenComparing_thenSortedByNameAge() {
Comparator<Employee> employee_Name_Age_Comparator
= Comparator.comparing(Employee::getName)
.thenComparingInt(Employee::getAge);

Arrays.sort(someMoreEmployees, employee_Name_Age_Comparator);

assertTrue(Arrays.equals(someMoreEmployees,
sortedEmployeesByNameAge));
}

После сортировки значения массива сотрудников имеют следующий порядок:

[Employee(name=Ace, age=22, salary=3000.0, mobile=6423001), 
Employee(name=Jake, age=22, salary=2000.0, mobile=5924001),
Employee(name=Jake, age=25, salary=3000.0, mobile=9922001),
Employee(name=Keith, age=35, salary=4000.0, mobile=3924401)]

Точно так же функции thenComparingLong и thenComparingDouble предназначены для использования длинных и двойных ключей сортировки соответственно.

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

Эта статья представляет собой руководство по нескольким функциям, представленным в Java 8 для интерфейса Comparator .

Как обычно, исходный код можно найти на Github .