ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Практика Stream API: отчёт по заказам и шпаргалка выбора операций

Автор: · Обновлено

Соберём отчёт по заказам: исключим возвраты, посчитаем выручку по категориям, отсортируем категории и проверим результат на нескольких наборах. Здесь Stream API решает цельную задачу; шпаргалка в конце помогает выбирать операции по форме данных.

Условие до первой цепочки

Цена хранится в целых копейках, количество должно быть положительным. Возвращённый заказ не участвует в выручке. Одинаковые строки считаются отдельными заказами: distinct применять нельзя. Пустой список даёт пустой отчёт. Категории обрезаются по краям и не могут быть пустыми, но регистр сохраняется — это осознанное правило примера.

Ожидаемый отчёт для четырёх заказов: books=3900, tools=2400; общая выручка 6300 копеек. Эти числа сначала получим вручную: 1200 × 2 + 1500 = 3900 для книг, 800 × 3 = 2400 для инструментов. Заказ на возвращённую книгу в сумме отсутствует.

Решение с явной проверкой входа

Сохраните полный файл SalesReport.java:

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class SalesReport {
    record Order(String category, int priceCents, int quantity, boolean returned) {
        Order {
            category = Objects.requireNonNull(category, "category").strip();
            if (category.isEmpty() || priceCents < 0 || quantity <= 0) {
                throw new IllegalArgumentException("Invalid order");
            }
        }

        long amountCents() {
            return Math.multiplyExact((long) priceCents, quantity);
        }
    }

    static Map<String, Long> revenue(List<Order> orders) {
        Objects.requireNonNull(orders, "orders");
        return orders.stream()
                .map(order -> Objects.requireNonNull(order, "order"))
                .filter(order -> !order.returned())
                .collect(Collectors.toMap(Order::category, Order::amountCents,
                        (left, right) -> Math.addExact(left, right), TreeMap::new));
    }

    static void check(Map<String, Long> expected, Map<String, Long> actual) {
        if (!expected.equals(actual)) {
            throw new AssertionError("Expected " + expected + ", got " + actual);
        }
    }

    public static void main(String[] args) {
        List<Order> orders = List.of(
                new Order("books", 1200, 2, false),
                new Order("tools", 800, 3, false),
                new Order("books", 1200, 1, true),
                new Order("books", 1500, 1, false));
        Map<String, Long> report = revenue(orders);
        check(Map.of("books", 3900L, "tools", 2400L), report);
        check(Map.of(), revenue(List.of()));
        check(Map.of(), revenue(List.of(new Order("books", 100, 1, true))));
        check(Map.of("books", 200L), revenue(List.of(
                new Order("books", 100, 1, false),
                new Order("books", 100, 1, false))));

        long total = report.values().stream().reduce(0L, Math::addExact);
        if (total != 6300L) throw new AssertionError("Wrong total");
        System.out.println(report);
        System.out.println("total=" + total);
        System.out.println("checks passed");
    }
}

Команды: javac -encoding UTF-8 --release 25 SalesReport.java, затем java SalesReport. Ожидаемый вывод:

{books=3900, tools=2400}
total=6300
checks passed

В компактном конструкторе record нормализуется параметр, из которого будет создан компонент. Неверный заказ отвергается при создании. Objects.requireNonNull для списка и элемента не позволяет случайно превратить отсутствие данных в пустую успешную выручку. Контракт Objects.requireNonNull).

Почему здесь toMap с объединением

Каждый оплаченный заказ сразу становится парой категория–сумма. При повторе категории складываются суммы, а TreeMap обеспечивает определённый порядок вывода. Мы не сохраняем списки заказов, потому что результату они не нужны. Collector задаёт правила сведения нескольких элементов к одному ключу. Перегрузки Collectors.toMap).

Приведение к long сделано до умножения, чтобы произведение не переполнило int ещё до присваивания. addExact явно отвергает переполнение общей суммы. Модель копеек подходит для этого упражнения без дробных ставок и округлений; для налогов, валют и произвольной точности потребуется отдельное правило денежной арифметики. Проверяемая арифметика Math.

Шпаргалка по форме задачи

ТребованиеОперацияЧто проверить до применения
Оставить оплаченные заказыfilterНе потеряны ли записи из-за неверного условия
Из заказа получить категориюmapМеняется тип элемента, а не число заказов
Объединить вложенные списки позицийflatMapНужны отдельные позиции, а не поток списков
Посчитать целые значенияmapToLong и подходящая редукцияДиапазон суммы и политика переполнения
Найти хотя бы один возвратanyMatchПустой источник даёт false
Найти первый подходящий заказfindFirstПорядок источника и отсутствие результата
Сгруппировать сами заказыgroupingByНужны списки, счётчики или другой downstream
Собрать суммы по ключуtoMap с merge-функциейКак разрешаются повторяющиеся ключи
Получить названия без повторовmap, затем distinctУникальны названия, а не события продаж
Выбрать верхние N после ранжированияsorted, затем limitСортировка происходит до ограничения
Получить итоговый списокtoListРезультат нельзя модифицировать
Получить изменяемый ArrayListtoCollection(ArrayList::new)Изменение списка не равно копии его объектов

Это маршруты выбора операции, а не утверждение, что любую задачу нужно писать одной цепочкой. Если требуются пошаговые решения, несколько связанных накопителей или подробная обработка каждой ошибки, обычный цикл может оказаться понятнее. Примеры операций Stream и варианты завершения.

Следующий шаг: проверяйте изменение постановки

Добавьте порог: в итоговом отчёте остаются категории с выручкой не меньше 3000 копеек. Применяйте его к уже посчитанным суммам; фильтрация отдельных заказов по цене решает другую задачу. Для исходных данных должен остаться только books=3900.

Добавьте проверки пустой категории, отрицательной цены, нулевого количества и null-элемента списка. Убедитесь, что каждая ошибка отвергается, а не превращается в нулевой доход. Затем напишите контрольный вариант через for и сравните результаты на том же наборе. Два коротких решения полезнее одного непроверяемого «красивого» pipeline.

Внешняя база, HTTP и parallelStream этому упражнению не нужны. Такое ограничение делает ожидаемый результат воспроизводимым и позволяет обсуждать именно операции над данными.

Частые вопросы

Почему не использовать double для цены?

Условие задаёт целые копейки, поэтому целочисленная модель выражает его прямо. Представление десятичных долей через binary floating point потребовало бы дополнительных правил округления, которые не относятся к цели этого упражнения.

Можно ли удалить одинаковые заказы через distinct?

В нашей постановке одинаковые поля не означают одну и ту же покупку. Для дедупликации нужен устойчивый идентификатор события и правило повторной доставки; без них distinct уменьшит реальную выручку.

Источники