Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Цепочка вызовов Java: Стримы

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

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 обрывается.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 21 ноября

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

Задавайте вопросы, если хотите обсудить теорию или упражнения. Команда поддержки Хекслета и опытные участники сообщества помогут найти ответы и решить задачу