Фильтрация и сортировка списка объектов с Stream API
Условие
Дан список сотрудников. Используя Stream API:
- Отфильтруйте сотрудников старше 30 лет
- Отсортируйте по зарплате по убыванию
- Возьмите топ-3
- Соберите их имена в список
class Employee {
String name;
int age;
double salary;
}
Пример
Вход: список из 10 сотрудников разного возраста и зарплаты
Выход: ["Alice", "Bob", "Charlie"] (имена топ-3 по зарплате среди тех, кому > 30)
Требования
- Используйте только Stream API
- Решение в одну цепочку вызовов
- Объясните каждую операцию
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Stream API: фильтрация, сортировка и сбор данных
Это классическая задача, демонстрирующая мощь Stream API в Java для функционального преобразования данных. Давайте разберёмся пошагово.
Полное решение в одну цепочку
List<String> topNames = employees.stream()
.filter(emp -> emp.age > 30) // шаг 1: фильтрация
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) // шаг 2: сортировка
.limit(3) // шаг 3: топ-3
.map(emp -> emp.name) // шаг 4: извлечение имён
.collect(Collectors.toList()); // шаг 5: сбор в список
Полный рабочий пример
import java.util.*;
import java.util.stream.Collectors;
class Employee {
String name;
int age;
double salary;
public Employee(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 + ")";
}
}
public class StreamExample {
public static void main(String[] args) {
// Создаём список сотрудников
List<Employee> employees = Arrays.asList(
new Employee("Alice", 35, 95000),
new Employee("Bob", 32, 85000),
new Employee("Charlie", 28, 65000),
new Employee("David", 40, 120000),
new Employee("Eve", 29, 60000),
new Employee("Frank", 38, 92000),
new Employee("Grace", 31, 78000),
new Employee("Henry", 25, 55000),
new Employee("Ivy", 45, 150000),
new Employee("Jack", 33, 88000)
);
// Применяем Stream API
List<String> topNames = employees.stream()
.filter(emp -> emp.getAge() > 30)
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.map(emp -> emp.getName())
.collect(Collectors.toList());
System.out.println(topNames);
// Вывод: [Ivy, David, Alice]
}
}
Подробное объяснение каждой операции
1. stream() — создание потока
Это интерминальная операция, которая преобразует коллекцию в поток. Stream — это последовательность элементов, которые можно обрабатывать функционально.
List<Employee> employees = ...;
Stream<Employee> stream = employees.stream();
2. filter(Predicate) — фильтрация
Фильтрует элементы по условию. Оставляет только те, для которых условие возвращает true. В нашем случае — сотрудники старше 30 лет.
.filter(emp -> emp.getAge() > 30)
Это промежуточная операция — она не выполняется немедленно, а запоминается.
3. sorted(Comparator) — сортировка
Сортирует элементы. Используем Comparator.comparingDouble() для сравнения по зарплате с методом reversed() для сортировки по убыванию.
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
В Java 8+ можно использовать:
// Вариант 1: через Comparator
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
// Вариант 2: через lambda
.sorted((e1, e2) -> Double.compare(e2.getSalary(), e1.getSalary()))
// Вариант 3: через custom Comparator
.sorted(Comparator.comparing(Employee::getSalary).reversed())
4. limit(long) — ограничение
Отбирает только первые N элементов потока. В нашем случае — топ-3.
.limit(3)
Это промежуточная операция.
5. map(Function) — преобразование
Преобразует каждый элемент потока. В нашем случае преобразуем объекты Employee в их имена (String).
.map(emp -> emp.getName())
Или эквивалентно с method reference:
.map(Employee::getName)
6. collect(Collector) — сбор результатов
Это терминальная операция — она действительно выполняет весь конвейер обработки. Собирает результаты в финальную коллекцию.
.collect(Collectors.toList())
Другие варианты сбора:
// Собрать в Set
.collect(Collectors.toSet())
// Собрать в String с разделителем
.collect(Collectors.joining(", "))
// Собрать в Map
.collect(Collectors.toMap(Employee::getName, Employee::getSalary))
// Собрать в LinkedList
.collect(Collectors.toCollection(LinkedList::new))
Альтернативные подходы
С использованием собственного Comparator
List<String> topNames = employees.stream()
.filter(emp -> emp.getAge() > 30)
.sorted((e1, e2) -> Double.compare(e2.getSalary(), e1.getSalary()))
.limit(3)
.map(Employee::getName)
.collect(Collectors.toList());
С предварительной сортировкой
List<String> topNames = employees.stream()
.filter(emp -> emp.getAge() > 30)
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.map(Employee::getName)
.limit(3) // limit можно переместить после map
.collect(Collectors.toList());
Примечание: для производительности лучше limit() размещать ДО map(), чтобы не преобразовывать лишние элементы.
Промежуточные vs Терминальные операции
Промежуточные (не выполняются сразу, запоминаются):
filter(),map(),sorted(),limit(),skip(),distinct()
Терминальные (выполняют весь конвейер):
collect(),forEach(),reduce(),count(),findFirst(),anyMatch()
// Без терминальной операции — ничего не выполнится
employees.stream()
.filter(emp -> emp.getAge() > 30) // запомнилась
.map(Employee::getName); // запомнилась
// Поток не выполнился!
// С терминальной операцией — выполняется конвейер
List<String> result = employees.stream()
.filter(emp -> emp.getAge() > 30)
.map(Employee::getName)
.collect(Collectors.toList()); // теперь выполнилось всё
Производительность и Lazy Evaluation
Stream API использует ленивые вычисления. Это означает, что промежуточные операции не выполняются пока не будет вызвана терминальная операция.
List<String> topNames = employees.stream()
.filter(emp -> {
System.out.println("filter: " + emp.getName());
return emp.getAge() > 30;
})
.limit(3) // это ограничение применяется во время выполнения
.map(emp -> {
System.out.println("map: " + emp.getName());
return emp.getName();
})
.collect(Collectors.toList());
// Вывод: фильтруются и обрабатываются только первые 3 элемента,
// а не все 10 из списка!
Это делает Stream API очень эффективным для больших данных.
Лучшие практики
// ✅ Хорошо: цепочка читается слева направо
List<String> result = employees.stream()
.filter(emp -> emp.getAge() > 30)
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.map(Employee::getName)
.collect(Collectors.toList());
// ✅ Хорошо: используем method references
.map(Employee::getName)
// ❌ Плохо: сложные условия в filter
.filter(emp -> emp.getAge() > 30 && emp.getSalary() > 70000 && ...)
// Лучше разбить на несколько filter
// ❌ Плохо: побочные эффекты в map
.map(emp -> {
System.out.println(emp.getName()); // побочный эффект!
return emp.getName();
})
// Для побочных эффектов используй forEach
Вывод
Stream API позволяет писать декларативный, функциональный код для обработки данных. Концепция цепочки операций (filter → sorted → limit → map → collect) делает код читаемым и выразительным. Понимание различия между промежуточными и терминальными операциями, а также ленивыми вычислениями, критично для эффективного использования Stream API в Java.