Как приручить хаос: сортируем ArrayList объектов по любому полю в Java
Представьте: вы пишете приложение для каталога книг. У вас есть класс Book с полями title, author, price и year. Вы складываете десятки книг в ArrayList. И тут пользователь просит: «Покажи сначала самые новые!», а через минуту — «Нет, отсортируй по цене!».
Если вы хоть раз сталкивались с такой задачей, то знаете — просто вызвать Collections.sort(list) не получится. Java не умеет читать мысли. Ей нужно объяснить: по какому именно полю сортировать. Именно об этом — самый популярный вопрос на Stack Overflow по теме сортировки кастомных объектов.
В этой статье мы разберём все способы сортировки ArrayList по свойству объекта: от классического Comparable до элегантных лямбда-выражений и Stream API. Поехали!
Подготовка: класс, который будем сортировать
Для примеров создадим простой класс Person:
public class Person {
private String name;
private int age;
private double salary;
public Person(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
// Геттеры (сеттеры опустим для краткости)
public String getName() { return name; }
public int getAge() { return age; }
public double getSalary() { return salary; }
@Override
public String toString() {
return name + " (" + age + " лет, $" + salary + ")";
}
}
И наполним наш список:
ArrayList<Person> people = new ArrayList<>();
people.add(new Person("Анна", 28, 55000));
people.add(new Person("Борис", 35, 72000));
people.add(new Person("Виктор", 22, 48000));
people.add(new Person("Галина", 30, 62000));
Теперь всё готово к экспериментам. Как отсортировать этот список по возрасту? По имени? По зарплате? У Java есть минимум три способа. Начнём с фундаментального.
Способ 1: Comparable — встраиваем логику сортировки прямо в класс
Интерфейс Comparable — это «естественный порядок» объекта. Если вы хотите, чтобы ваши объекты всегда сортировались по одному полю (например, по возрасту), реализуйте этот интерфейс.
Изменим наш класс Person:
public class Person implements Comparable<Person> {
// ... поля, конструктор, геттеры ...
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
Теперь сортировка — одна строка:
Collections.sort(people);
System.out.println(people);
// Вывод: [Виктор (22 лет, $48000), Анна (28 лет, $55000), Галина (30 лет, $62000), Борис (35 лет, $72000)]
Плюсы: простота, не нужно думать о внешних компараторах.
Минусы: вы «зашиваете» в класс только один способ сортировки. Если завтра понадобится сортировать по имени — придётся менять класс.
Кстати, для обратного порядка (от большего к меньшему) можно использовать Collections.reverseOrder():
Collections.sort(people, Collections.reverseOrder());
Способ 2: Comparator — гибкость без изменения класса
Когда нужно сортировать по-разному в разных местах программы, на помощь приходит Comparator. Это отдельный класс или анонимный класс, который содержит логику сравнения.
Анонимный класс — дедушкин метод
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
});
Работает, но выглядит громоздко. В современной Java так не пишут.
Лямбда-выражения — лаконичность и мощь
Java 8 принесла лямбды. Сравнение по имени умещается в одну строку:
people.sort((p1, p2) -> p1.getName().compareTo(p2.getName()));
А если нужно по убыванию? Легко:
people.sort((p1, p2) -> p2.getAge() - p1.getAge());
Но будьте осторожны с вычитанием для чисел с плавающей точкой! Для salary используйте Double.compare:
people.sort((p1, p2) -> Double.compare(p1.getSalary(), p2.getSalary()));
Статические методы Comparator — ещё удобнее
Самый элегантный способ — использовать геттеры и ссылки на методы:
// По возрасту (по возрастанию)
people.sort(Comparator.comparingInt(Person::getAge));
// По имени (по алфавиту)
people.sort(Comparator.comparing(Person::getName));
// По зарплате (по убыванию)
people.sort(Comparator.comparingDouble(Person::getSalary).reversed());
Обратите внимание: comparingInt и comparingDouble — специализированные версии для примитивов, они работают быстрее.
Сортировка по нескольким полям
Хотите отсортировать сначала по возрасту, а если возраст одинаковый — по имени? thenComparing к вашим услугам:
people.sort(Comparator.comparingInt(Person::getAge)
.thenComparing(Person::getName));
Можно добавлять сколько угодно полей:
people.sort(Comparator.comparing(Person::getName)
.thenComparingInt(Person::getAge)
.thenComparingDouble(Person::getSalary).reversed());
Способ 3: Stream API — сортировка без изменения исходного списка
Иногда не нужно менять оригинальный список, а достаточно получить отсортированный поток данных. Тут в игру вступает Stream.sorted():
List<Person> sortedByAge = people.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
Оригинальный список people остался нетронутым. Очень полезно, когда исходные данные нужны в первоначальном порядке.
Можно комбинировать с другими операциями:
people.stream()
.filter(p -> p.getAge() > 25)
.sorted(Comparator.comparing(Person::getName))
.forEach(System.out::println);
Это выведет только людей старше 25 лет, отсортированных по имени, не изменяя исходный список.
Какой способ выбрать? Сравнение и рекомендации
Давайте подведём итог и выберем инструмент под задачу:
- Comparable — используйте, если у объекта есть один «естественный» порядок (например, дата для событий, ID для записей в