Вспомним код из предыдущего урока, в котором мы вычисляли стоимость товаров в списке.
var products = List.of("Laptop: 800", "Headphones: 50", "Smartphone: 500", "Mouse: 20");
var totalPrice = products.stream()
.reduce(0,
(sum, product) -> {
var parts = product.split(": ");
var price = Integer.parseInt(parts[1].trim());
return sum + price;
},
Integer::sum);
Если сформулировать словами то, что здесь происходит то мы получим такой алгоритм:
- Извлекаем цены из списка товаров.
- Находим общую стоимость.
Если присмотреться, то можно увидеть, что первая операция очень похожа на отображение. То есть, мы здесь имеем дело не с одной сверткой, а с двумя последовательными операциями. Перепишем код таким образом.
var products = List.of("Laptop: 800", "Headphones: 50", "Smartphone: 500", "Mouse: 20");
var totalPrice = products.stream()
.map((product) -> {
var parts = product.split(": ");
var price = Integer.parseInt(parts[1].trim());
return price;
})
.reduce(0, Integer::sum);
Так как map()
возвращает Stream
, то мы можем сразу продолжить нашу цепочку вызовов.
В этом разделении кроется одна из ключевых особенностей использования стримов, которая помогает делать код проще и понятнее. Разделение на независимые этапы позволяет разбить сложную операцию таким образом, что на каждом этапе понадобится думать только о небольшой операции, которую легко проанализировать. Такого же эффекта нельзя добиться с циклами, так как циклы не комбинируются, каждый цикл живет своей собственной жизнью в отличие от стрима.
Превращение сложной операции в набор простых требует времени на освоения, так как по началу не всегда очевидно, что операцию можно разбить. Рассмотрим еще несколько примеров, которые помогут нам начать использовать стримы правильно.
Допустим, что нам надо вычислить сумму чисел списка, но только тех чисел, которые больше 5. Эта задача разбивается на две:
- Фильтрация. Оставляем числа больше 5.
- Свертка. Ищем сумму.
var numbers = List.of(1, 2, 3, 4, 5);
var sum = numbers.stream()
.filter((number) -> number > 5)
.reduce(0, Integer::sum);
System.out.println(sum);
Предположим, что у нас есть список сотрудников какой-то компании и мы хотим посчитать количество денег, которые тратятся на зарплаты в одном из подразделений. Эта задача распадается на три этапа.
- Фильтрация. Оставляем сотрудников только для нужного подразделения.
- Отображение. Извлекаем зарплату.
- Свертка. Считаем общую сумму.
// Предположим, что у нас есть класс Employee
// Параметры конструктора: имя, департамент, зарплата
var employees = List.of(
new Employee("John Doe", "IT", 70000),
new Employee("Jane Smith", "IT", 75000),
new Employee("Mary Johnson", "HR", 60000),
new Employee("Mike Wilson", "Marketing", 65000)
);
var totalItSalary = employees
.stream()
.filter(e -> "IT".equals(e.getDepartment()))
.map(Employee::getSalary)
.reduce(0, Integer::sum);
System.out.println(totalItSalary); // 145000
Ленивое выполнение
Глядя на последний пример, может возникнуть вопрос, а не слишком ли расточительно обходить столько раз список, во время фильтрации, во время отображения и при свертке? В действительности список обходится ровно один раз. Происходит это потому, что стримы в Java ленивые. То есть, несмотря на вызов методов filter()
и map()
их реальный вызов не начинается до тех пор, пока эти данные не понадобятся. При этом внутри все реализовано таким образом, что выполняется один проход, во время которого данные пропускаются через всю цепочку функций.
В том числе по этой причине мы вызывали toList()
, когда рассматривали map()
и filter()
. Этот метод запускает процесс вычисления. reduce()
тоже запускает вычисление, так как это терминальная операция, на которой stream
обрывается.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.