Практические примеры использования Stream API

от автора

Всем привет! Решил написать статью с практическими примерами использования Stream API. В данной статье не будет теории — только хардкор и практические примеры. Поехали!

В качестве «подопытного кролика» буду использовать проект, который можете найти здесь.

Для тестирования буду использовать Postman.

В проекте есть три сущности с которыми мы будем работать: Client, Product и Booking. Связи между ними можно посмотреть на следующей картинке.

В сущности Product есть enum Category. Центральная сущность — Booking, в которую входит Client, List<Product>, а также enum Status. То есть Booking — это заказ перечня продуктов, который делает клиент.

Я в проекте буду использовать базу данных H2. Для просмотра данных можно после запуска проекта пройти по ссылке http://localhost:8080/h2-console , ввести там username: user и password: password и вы увидите это:

Доступ к данной штуке происходит за счет spring.h2.console.enabled=true, которую я прописал в application.properties, здесь также можно поменять логин и пароль доступа к базе данных.

Генерацию таблиц сделает за нас Hibernate за счет spring.jpa.hibernate.ddl-auto=create, которая прописана в application.properties, так как прописано create — при каждом запуске проекта будут удалятся и создаваться заново таблицы в базе данных. Это можно увидеть в логах так как прописаны spring.jpa.show-sql=true и spring.jpa.properties.hibernate.format_sql=true.

Для генерации данных в таблицы базу я написал следующих класс

@Component public class DataLoader {     private final long NUMBER_BOOKINGS = 30;      @Bean     public CommandLineRunner loadDataClient(ClientRepository clientRepository, ProductRepository productRepository, BookingRepository bookingRepository) {         return (args) -> {             Faker faker = new Faker();             long indexForProduct = 1;             for (long i = 0; i <= NUMBER_BOOKINGS; i++) {                 Set<Product>products = new HashSet<>();                 long indexToIncreaseProduct =0;                 for (long j = indexForProduct; j <= faker.number().numberBetween(indexForProduct, indexForProduct+10); j++) {                     indexToIncreaseProduct = j;                         Product product = productRepository.save(new Product(j, faker.lorem().word(),                                 Category.valueOf(Category.values()[faker.number().numberBetween(0, 5)].name()),                                 faker.number().randomDouble(2, 1, 100)));                         products.add(product);                 }                 indexForProduct += indexToIncreaseProduct;                  Client client = clientRepository.save(new Client(i, faker.name().firstName() + " " + faker.name().lastName()));                  bookingRepository.save(new Booking(i, LocalDate.of(2022, Month.JANUARY, 1).plusDays(faker.number().numberBetween(1, 365)), Status.valueOf(Status.values()[faker.number().numberBetween(0, 3)].name()),                         client, products));             }           };     } }

Данный код генерирует 30 заказов, клиентов и случайное количество продуктов в заказе и сохраняет все это в базу данных. Число заказов можете менять на свое усмотрение. Для генерации данных я использовал стороннюю библиотеку Faker, кому интересно можете про нее почитать тут https://www.baeldung.com/java-faker.

С вводной частью все можно приступать к Stream API.


Пример 1. Найти все продукты категории «Book», с ценой более 50

@Override     public List<Product> findAllProductsBelongsCategoryBookWithPriceMore50() {         return productRepository.findAll()                 .stream()                 .filter(product -> product.getCategory().equals(Category.BOOK))                 .filter(product -> product.getPrice() > 50)                 .collect(Collectors.toList());     }

Находим все продукты в базе данных, потом применяем два фильтра, первый оставляет нам только продукты категории «Book», а второй сравнивает цену с условием и оставляет только продукты, где цена больше 50. В конце делаем терминальную операцию и собираем наш поток в список продуктов, но уже только с теми, которые прошли все фильтры.

Для тестирования данного кода можете в Postman отправить запрос http://localhost:8080/api/v1/product/findAllProductsBelongsCategoryBookWithPriceMore50

и получите

или что-то похожее, так как первоначальные данные будут у всех генерироваться разные. Также для тестирования всех примеров есть два контроллера (BookingController и ProductController) с написанными эндпоинтами, вызывая которые можно протестировать примеры.

Пример 2. Найти все продукты, заказанные за определенный, заданный период времени

@Override     public List<Product> findAllProductOrderedBetweenDates(LocalDate start, LocalDate finish) {         return bookingRepository.findAll()                 .stream()                 .filter(booking -> booking.getOrderDate().compareTo(start) >= 0)                 .filter(booking -> booking.getOrderDate().compareTo(finish) <= 0)                 .flatMap(booking -> booking.getProducts().stream())                 .collect(Collectors.toList());     }

Находим все заказы, после применяем два фильтра, которые фильтруют наши заказы, чтобы дальше прошли только заказанные в заданный период.  После применяем flatMap(), которая делает один новый stream из всех продуктов в заказах. Последняя – это терминальная операция, которая объединяет все продукты в один список.

Для тестирования этого примера нам необходимо в Postman, также передать даты периода, за который мы хотим сделать выборку.

Пример 3. Найти самый дешевый продукт в категории «Medicine»

 @Override     public Optional<Product> findCheapestProductInCategoryMedicine() {         return productRepository.findAll()                 .stream()                 .filter(product -> product.getCategory().equals(Category.MEDICINE))                 .min(Comparator.comparing(Product::getPrice));     }

Получаем stream со всеми продуктами, потом применяем фильтр, который оставляет только продукты категории “Medicine” и потом с помощью операции min() находим самый дешевый продукт.

Пример 4. Найти все продукты, заказанные в определенный день

    @Override     public List<Product> findAllProductOrderedInDate(LocalDate date) {         return bookingRepository.findAll()                 .stream()                 .filter(booking -> booking.getOrderDate().isEqual(date))                 .flatMap(booking -> booking.getProducts().stream())                 .collect(Collectors.toList());     }

Находим все заказы, после применяем фильтр, где мы сравниваем дату нашего заказа с заданной датой.  После применяем flatMap(), которая делает один новый stream из всех продуктов в заказах. Последняя – это терминальная операция, которая объединяет все продукты в один список.

Пример 5. Получить статистические данные по продуктам категории “Food”

    @Override     public DoubleSummaryStatistics obtainCollectionOfStaticAllProductsCategoryFood() {         return productRepository.findAll()                 .stream()                 .filter(product -> product.getCategory().equals(Category.FOOD))                 .mapToDouble(Product::getPrice)                 .summaryStatistics();      }

Получаем stream со всеми продуктами, потом применяем фильтр, который оставляет только продукты категории “ Food ”, а затем с помощью операции mapToDouble() образуем один поток данных с ценами всех продуктов данной категории и после с помощью терминальной операции summaryStatistics() получаем статистические данные.

Отправив в Postman запрос http://localhost:8080/api/v1/product/obtainCollectionOfStaticAllProductsCategoryFood мы получим статистические данные:

у вас конкретные значения будут отличаться.

Пример 6. Сгруппировать продукты по категориям

@Override     public Map<String, List<String>> getMapWithListProductsNameByCategory() {         return productRepository.findAll()                 .stream()                 .collect(Collectors.groupingBy(                         product -> product.getCategory().name(),                         Collectors.mapping(Product::getName, Collectors.toList())                 ));     }

Первоначально получаем список всех продуктов и делаем из него поток. После с помощью операции groupingBy() задаем по чем мы хотим группировать продукты (по имени категории) и с помощью операции mapping() указываем какие данные должны быть собраны в список и относиться к данной категории.

Пример 7. Получить самые дорогие продукты по категориям

 @Override     public Map<String, Optional<Product>> getMostExpensiveProductByCategory() {         return productRepository.findAll()                 .stream()                 .collect(                         Collectors.groupingBy(                         product -> product.getCategory().name(),                                 Collectors.maxBy(Comparator.comparing(Product::getPrice)))                 );     }

Первоначально получаем список всех продуктов и делаем из него поток. После с помощью операции groupingBy() задаем по чем мы хотим группировать продукты (по имени категории) и с помощью операции maxBy() указываем какой продукт (в нашем случае самый дорогой) должен относиться к данной категории.

Пример 8. Получить все заказы, которые принадлежат категории “Sport”

@Override     public List<Booking> findAllBookingWithProductsBelongCategorySport() {         return bookingRepository.findAll()                 .stream()                 .filter(booking -> booking.getProducts().stream().anyMatch(product -> product.getCategory().equals(Category.SPORT)))                 .collect(Collectors.toList());     }

Находим все заказы и делаем из него поток. После применяем фильтр, в котором из каждого заказа достаем список продуктов и делаем из него поток и с помощью операции anyMatch() проверяем есть ли в данном списке хотя бы один продукт с категорией “Sport”. После все собираем в один список с помощью терминальной операции collect().

Пример 9. Получить три последние заказы

@Override     public List<Booking> findThreeMostRecentBooking() {         return bookingRepository.findAll()                 .stream()                 .sorted(Comparator.comparing(Booking::getOrderDate).reversed())                 .limit(3)                 .collect(Collectors.toList());     }

Получаем список заказов и делаем из него поток, далее сортируем данный поток с помощью операции sorted(), где в качестве того по чем мы будем сортировать передаем дату заказа (Booking::getOrderDate) и так как список будет отсортирован от самого старого заказа до самого последнего, то необходимо применить операцию reserved(), чтобы список развернуть в обратном направлении – от самого нового заказа до самого старого. Операция limit() оставляет только нужное количество нам элементов и последняя терминальная операция собирает все в список.

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

 @Override     public Double calculateTotalSumAllBookingsBetweenDates(LocalDate start,                                                            LocalDate finish) {         return bookingRepository.findAll()                 .stream()                 .filter(booking -> booking.getOrderDate().compareTo(start) >= 0)                 .filter(booking -> booking.getOrderDate().compareTo(finish) < 0)                 .flatMap(booking -> booking.getProducts().stream())                 .mapToDouble(Product::getPrice)                 .sum();     }

Получаем список заказов, после применяем два фильтра, с помощью которых оставляем только заказы с нужного нам диапазона времени. Затем с помощью операции flatMap() делаем общий поток всех продуктов и применяем операцию mapToDouble(), которая делает из общего потока продуктов поток цен. Завершающая терминальная операция sum() суммирует все числа.

Пример 11. Найти среднее значение стоимости всех заказов со статусом APPROVED за определенную дату

 @Override     public Double calculateAverageAllBookingsWithStatusApprovedOnDate(LocalDate start) {         return bookingRepository.findAll()                 .stream()                 .filter(booking -> booking.getOrderDate().isEqual(start))                 .filter(booking -> booking.getStatus().equals(Status.APPROVED))                 .flatMap(booking -> booking.getProducts().stream())                 .mapToDouble(Product::getPrice)                 .average()                 .orElse(0);     }

Находим все заказы, после применяем два фильтра. Первый оставляет только заказы за определенную дату, а второй выбирает только заказы со статусом APPROVED. Далее с помощью операции flatMap() из всех заказов достаем списки продуктов и делаем из них общий поток и с помощью операции mapToDouble(), делам из общего потока продуктов поток их цен. Завершающая терминальная операция average() находит среднее значение данных цен.

Пример 12. Получить Map<>, где ключ – Id заказа, а значение —  количество продуктов в данном заказе

@Override     public Map<Long, Integer> getMapWithBookingIdAndCountProduct() {         return bookingRepository.findAll()                 .stream()                 .collect(Collectors.toMap(Booking::getId, booking -> booking.getProducts().size()));     }

Получаем список всех заказов и делаем из него поток. После применяем операцию collect(), где указываем, что хотим собрать все в Map<> и указываем, что ключом будет являться Id заказа, а значением —  количество продуктов в данном заказе.

Пример 13. Получить Map<>, где ключ – клиент, а значение —  список заказов, которые сделал данный клиент

    @Override     public Map<Client, List<Booking>> getMapWithClientAndListBookings() {         return bookingRepository.findAll()                 .stream()                 .collect(Collectors.groupingBy(Booking::getClient));     }

Получаем список всех заказов и делаем из него поток. После применяем операцию collect(), где указываем, что хотим сгруппировать по клиентам и все. На сколько упрощается код при использовании stream API.

Пример 14. Получить Map<>, где ключ – заказ, а значение — общая сумма стоимости всех продуктов в нем

    @Override     public Map<Booking, Double> getMapWithBookingAndProductTotalSum() {         return bookingRepository.findAll()                 .stream()                 .collect(Collectors.toMap(                         Function.identity(),                         booking -> booking.getProducts().stream()                                 .mapToDouble(Product::getPrice).sum()                 ));     }

Получаем список всех заказов и делаем из него поток. После применяем операцию collect(), где указываем, что хотим собрать все в Map<> и указываем, что ключом будет являться заказ (Function.identity()), а значением —  мы из всех заказов достаем списки продуктов и делаем из них общий поток и с помощью операции mapToDouble(), делам из общего потока продуктов поток их цен. Завершающая терминальная операция sum() находит сумму данных цен.

Всем спасибо кто дочитал до конца.

Всем удачи в изучении stream API.


ссылка на оригинал статьи https://habr.com/ru/post/684912/