Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.3.2. Промежуточные операции

filter(), map(), flatMap(), sorted(), distinct(), peek(), limit(), skip()

Материалы

ТипСсылка
ДокументStream Operations
Видеоссылка

Что такое промежуточные операции?

Промежуточные операции (intermediate operations) - это операции, которые:

  • Возвращают новый Stream
  • Ленивые (lazy) - выполняются только при вызове терминальной операции
  • Можно объединять в цепочки (pipeline)
Источник → filter() → map() → sorted() → [терминальная операция]
              ↑          ↑        ↑
         промежуточные операции (lazy)

Stateless vs Stateful операции

Stateless (без состояния):

  • Обрабатывают каждый элемент независимо
  • filter(), map(), flatMap(), peek()

Stateful (с состоянием):

  • Требуют знания о других элементах
  • sorted(), distinct(), limit(), skip()
  • Могут требовать буферизации всего потока

filter() - Фильтрация элементов

Оставляет только элементы, удовлетворяющие условию (predicate).

Сигнатура

Stream<T> filter(Predicate<? super T> predicate)

Базовые примеры

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Только четные числа
List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .toList();  // [2, 4, 6, 8, 10]

// Только положительные
List<Integer> positive = numbers.stream()
    .filter(n -> n > 0)
    .toList();

// Фильтрация строк
List<String> names = List.of("Alice", "Bob", "Anna", "Alex");
List<String> startsWithA = names.stream()
    .filter(name -> name.startsWith("A"))
    .toList();  // [Alice, Anna, Alex]

Множественные условия

// Несколько filter() подряд
List<Integer> result = numbers.stream()
    .filter(n -> n > 3)
    .filter(n -> n < 8)
    .filter(n -> n % 2 == 0)
    .toList();  // [4, 6]

// Или объединенный predicate
List<Integer> result = numbers.stream()
    .filter(n -> n > 3 && n < 8 && n % 2 == 0)
    .toList();  // [4, 6]

Фильтрация объектов

record Person(String name, int age, String city) {}

List<Person> people = List.of(
    new Person("Alice", 30, "Moscow"),
    new Person("Bob", 25, "SPb"),
    new Person("Charlie", 35, "Moscow")
);

// Взрослые из Москвы
List<Person> result = people.stream()
    .filter(p -> p.age() >= 30)
    .filter(p -> p.city().equals("Moscow"))
    .toList();

Фильтрация с null-безопасностью

List<String> items = Arrays.asList("a", null, "b", null, "c");

// Убрать null
List<String> nonNull = items.stream()
    .filter(Objects::nonNull)
    .toList();  // [a, b, c]

// Или через лямбду
List<String> nonNull = items.stream()
    .filter(s -> s != null)
    .toList();

Использование method reference

// Вместо лямбды
List<String> nonEmpty = strings.stream()
    .filter(s -> !s.isEmpty())
    .toList();

// Method reference с Predicate.not()
List<String> nonEmpty = strings.stream()
    .filter(Predicate.not(String::isEmpty))
    .toList();

map() - Преобразование элементов

Преобразует каждый элемент в другой элемент (возможно другого типа).

Сигнатура

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Базовые примеры

List<String> names = List.of("alice", "bob", "charlie");

// К верхнему регистру
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .toList();  // [ALICE, BOB, CHARLIE]

// Длина каждой строки
List<Integer> lengths = names.stream()
    .map(String::length)
    .toList();  // [5, 3, 7]

// Числа -> их квадраты
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
    .map(n -> n * n)
    .toList();  // [1, 4, 9, 16, 25]

Преобразование типов

// String -> Integer
List<String> strNumbers = List.of("1", "2", "3");
List<Integer> integers = strNumbers.stream()
    .map(Integer::parseInt)
    .toList();  // [1, 2, 3]

// Person -> String (имя)
List<String> names = people.stream()
    .map(Person::name)
    .toList();

// Person -> PersonDTO
List<PersonDTO> dtos = people.stream()
    .map(p -> new PersonDTO(p.name(), p.age()))
    .toList();

Цепочки map()

List<String> result = names.stream()
    .map(String::trim)           // Убрать пробелы
    .map(String::toLowerCase)    // К нижнему регистру
    .map(s -> s + "!")           // Добавить суффикс
    .toList();

mapToInt(), mapToLong(), mapToDouble()

Для преобразования в примитивные стримы (избегают boxing).

List<String> words = List.of("one", "two", "three");

// Stream<String> -> IntStream
int totalLength = words.stream()
    .mapToInt(String::length)
    .sum();  // 11

// Среднее значение
double avgLength = words.stream()
    .mapToInt(String::length)
    .average()
    .orElse(0.0);  // 3.67

// Сумма возрастов
int totalAge = people.stream()
    .mapToInt(Person::age)
    .sum();

flatMap() - Выравнивание вложенных структур

Преобразует каждый элемент в Stream и объединяет все потоки в один.

Сигнатура

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

Визуализация

map():     [1, 2] -> [[1], [2]]           (Stream of Streams)
flatMap(): [1, 2] -> [1, 2]               (плоский Stream)

Пример:
[[a, b], [c, d, e]] -> flatMap -> [a, b, c, d, e]

Базовые примеры

// Список списков -> плоский список
List<List<Integer>> nested = List.of(
    List.of(1, 2, 3),
    List.of(4, 5),
    List.of(6, 7, 8, 9)
);

List<Integer> flat = nested.stream()
    .flatMap(List::stream)
    .toList();  // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Массивы
String[][] arrays = {{"a", "b"}, {"c", "d", "e"}};
List<String> flat = Arrays.stream(arrays)
    .flatMap(Arrays::stream)
    .toList();  // [a, b, c, d, e]

Разделение строк

List<String> sentences = List.of(
    "Hello world",
    "Java Stream API",
    "flatMap example"
);

// Все слова
List<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")))
    .toList();
// [Hello, world, Java, Stream, API, flatMap, example]

// Все символы
List<Character> chars = sentences.stream()
    .flatMap(s -> s.chars().mapToObj(c -> (char) c))
    .toList();

flatMap с Optional

List<Optional<String>> optionals = List.of(
    Optional.of("a"),
    Optional.empty(),
    Optional.of("b"),
    Optional.empty(),
    Optional.of("c")
);

// Извлечь только присутствующие значения
List<String> values = optionals.stream()
    .flatMap(Optional::stream)
    .toList();  // [a, b, c]

Практический пример: заказы и товары

record Order(String id, List<Item> items) {}
record Item(String name, double price) {}

List<Order> orders = List.of(
    new Order("1", List.of(new Item("Book", 20), new Item("Pen", 5))),
    new Order("2", List.of(new Item("Laptop", 1000))),
    new Order("3", List.of(new Item("Mouse", 50), new Item("Keyboard", 100)))
);

// Все товары из всех заказов
List<Item> allItems = orders.stream()
    .flatMap(order -> order.items().stream())
    .toList();

// Все названия товаров
List<String> itemNames = orders.stream()
    .flatMap(order -> order.items().stream())
    .map(Item::name)
    .toList();  // [Book, Pen, Laptop, Mouse, Keyboard]

// Общая сумма всех заказов
double total = orders.stream()
    .flatMap(order -> order.items().stream())
    .mapToDouble(Item::price)
    .sum();  // 1175.0

flatMapToInt(), flatMapToLong(), flatMapToDouble()

List<int[]> arrays = List.of(
    new int[]{1, 2},
    new int[]{3, 4, 5}
);

int sum = arrays.stream()
    .flatMapToInt(Arrays::stream)
    .sum();  // 15

sorted() - Сортировка

Сортирует элементы потока.

Сигнатуры

Stream<T> sorted()                          // natural order
Stream<T> sorted(Comparator<? super T> c)   // custom order

Natural ordering

List<Integer> numbers = List.of(5, 2, 8, 1, 9, 3);

// По возрастанию (natural order)
List<Integer> sorted = numbers.stream()
    .sorted()
    .toList();  // [1, 2, 3, 5, 8, 9]

// Строки - лексикографически
List<String> names = List.of("Charlie", "Alice", "Bob");
List<String> sorted = names.stream()
    .sorted()
    .toList();  // [Alice, Bob, Charlie]

Custom Comparator

// По убыванию
List<Integer> descending = numbers.stream()
    .sorted(Comparator.reverseOrder())
    .toList();  // [9, 8, 5, 3, 2, 1]

// По длине строки
List<String> byLength = names.stream()
    .sorted(Comparator.comparing(String::length))
    .toList();  // [Bob, Alice, Charlie]

// По длине, затем алфавитно
List<String> sorted = names.stream()
    .sorted(Comparator.comparing(String::length)
                      .thenComparing(Comparator.naturalOrder()))
    .toList();

Сортировка объектов

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
);

// По возрасту
List<Person> byAge = people.stream()
    .sorted(Comparator.comparing(Person::age))
    .toList();

// По имени в обратном порядке
List<Person> byNameDesc = people.stream()
    .sorted(Comparator.comparing(Person::name).reversed())
    .toList();

// По возрасту, затем по имени
List<Person> sorted = people.stream()
    .sorted(Comparator.comparing(Person::age)
                      .thenComparing(Person::name))
    .toList();

Сортировка с null

List<String> withNulls = Arrays.asList("b", null, "a", null, "c");

// null в начало
List<String> sorted = withNulls.stream()
    .sorted(Comparator.nullsFirst(Comparator.naturalOrder()))
    .toList();  // [null, null, a, b, c]

// null в конец
List<String> sorted = withNulls.stream()
    .sorted(Comparator.nullsLast(Comparator.naturalOrder()))
    .toList();  // [a, b, c, null, null]

Важно: sorted() - stateful операция

// sorted() буферизует все элементы перед сортировкой
// Не подходит для бесконечных потоков!

Stream.iterate(0, n -> n + 1)
    .sorted()  // Зависнет - пытается собрать бесконечный поток
    .limit(10)
    .forEach(System.out::println);

// Правильно: сначала limit(), потом sorted()
Stream.iterate(0, n -> n + 1)
    .limit(10)
    .sorted()
    .forEach(System.out::println);

distinct() - Удаление дубликатов

Удаляет повторяющиеся элементы (по equals()).

Сигнатура

Stream<T> distinct()

Базовые примеры

List<Integer> numbers = List.of(1, 2, 2, 3, 3, 3, 4, 4, 4, 4);

List<Integer> unique = numbers.stream()
    .distinct()
    .toList();  // [1, 2, 3, 4]

// Строки
List<String> words = List.of("apple", "banana", "apple", "cherry", "banana");
List<String> unique = words.stream()
    .distinct()
    .toList();  // [apple, banana, cherry]

Порядок сохраняется

// distinct() сохраняет порядок первого появления
List<Integer> numbers = List.of(3, 1, 2, 1, 3, 2);
List<Integer> unique = numbers.stream()
    .distinct()
    .toList();  // [3, 1, 2] - порядок первого появления

distinct() для объектов

Важно: distinct() использует equals() и hashCode().

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Alice", 30),  // дубликат
    new Person("Charlie", 35)
);

List<Person> unique = people.stream()
    .distinct()
    .toList();  // 3 элемента (Alice,30 только один раз)

Уникальность по полю

// distinct() по конкретному полю - нужен workaround
// Способ 1: через Set
Set<String> seen = new HashSet<>();
List<Person> uniqueByName = people.stream()
    .filter(p -> seen.add(p.name()))  // add возвращает false если уже есть
    .toList();

// Способ 2: через Collectors.toMap
List<Person> uniqueByName = people.stream()
    .collect(Collectors.toMap(
        Person::name,
        p -> p,
        (p1, p2) -> p1  // при дубликате - оставить первый
    ))
    .values()
    .stream()
    .toList();

peek() - Отладка и побочные эффекты

Выполняет действие для каждого элемента без изменения потока.

Сигнатура

Stream<T> peek(Consumer<? super T> action)

Отладка pipeline

List<Integer> result = List.of(1, 2, 3, 4, 5).stream()
    .filter(n -> n > 2)
    .peek(n -> System.out.println("После filter: " + n))
    .map(n -> n * 2)
    .peek(n -> System.out.println("После map: " + n))
    .toList();

// Вывод:
// После filter: 3
// После map: 6
// После filter: 4
// После map: 8
// После filter: 5
// После map: 10

Предупреждение: peek() не гарантирует выполнение

// peek() может не выполниться для всех элементов!
List.of(1, 2, 3).stream()
    .peek(System.out::println);  // Ничего не выведет - нет терминальной операции

// Даже с терминальной операцией - зависит от оптимизаций
List.of(1, 2, 3).stream()
    .peek(System.out::println)
    .count();  // Может не вызвать peek() (оптимизация в Java 9+)

Когда использовать peek()

peek() предназначен для отладки.
Не используй для побочных эффектов в production коде!
// Плохо - побочный эффект в peek()
List<String> collected = new ArrayList<>();
stream.peek(collected::add)  // Непредсказуемо!
      .count();

// Хорошо - используй forEach или collect
stream.forEach(collected::add);
// или
List<String> collected = stream.toList();

limit() - Ограничение количества элементов

Ограничивает поток первыми N элементами.

Сигнатура

Stream<T> limit(long maxSize)

Базовые примеры

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Первые 3 элемента
List<Integer> first3 = numbers.stream()
    .limit(3)
    .toList();  // [1, 2, 3]

// Топ-5 после сортировки
List<Integer> top5 = numbers.stream()
    .sorted(Comparator.reverseOrder())
    .limit(5)
    .toList();  // [10, 9, 8, 7, 6]

limit() с бесконечными потоками

// Без limit() - бесконечный цикл
Stream.generate(Math::random)
    .forEach(System.out::println);  // Никогда не закончится

// С limit() - работает
List<Double> randomNumbers = Stream.generate(Math::random)
    .limit(5)
    .toList();  // 5 случайных чисел

// Последовательность чисел
List<Integer> sequence = Stream.iterate(0, n -> n + 1)
    .limit(10)
    .toList();  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Short-circuiting

limit() - это short-circuiting операция: она может завершить обработку раньше.

List.of(1, 2, 3, 4, 5).stream()
    .peek(n -> System.out.println("Processing: " + n))
    .limit(3)
    .toList();

// Вывод:
// Processing: 1
// Processing: 2
// Processing: 3
// Элементы 4 и 5 не обрабатываются!

skip() - Пропуск элементов

Пропускает первые N элементов.

Сигнатура

Stream<T> skip(long n)

Базовые примеры

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Пропустить первые 3
List<Integer> result = numbers.stream()
    .skip(3)
    .toList();  // [4, 5, 6, 7, 8, 9, 10]

// Пропустить больше чем есть
List<Integer> result = numbers.stream()
    .skip(100)
    .toList();  // [] (пустой список)

Пагинация: skip() + limit()

int pageSize = 5;
int pageNumber = 2;  // начиная с 0

List<Integer> page = numbers.stream()
    .skip((long) pageNumber * pageSize)
    .limit(pageSize)
    .toList();  // Страница 2 (индекс с 0): элементы 10-14

Пример пагинации

public <T> List<T> getPage(List<T> items, int page, int size) {
    return items.stream()
        .skip((long) page * size)
        .limit(size)
        .toList();
}

// Использование
List<String> items = List.of("a", "b", "c", "d", "e", "f", "g", "h");
List<String> page0 = getPage(items, 0, 3);  // [a, b, c]
List<String> page1 = getPage(items, 1, 3);  // [d, e, f]
List<String> page2 = getPage(items, 2, 3);  // [g, h]

takeWhile() и dropWhile() (Java 9+)

takeWhile() - брать пока условие true

Stream<T> takeWhile(Predicate<? super T> predicate)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 1, 2);

// Брать пока < 4
List<Integer> result = numbers.stream()
    .takeWhile(n -> n < 4)
    .toList();  // [1, 2, 3]

// Отличие от filter():
// filter() проверяет ВСЕ элементы
// takeWhile() останавливается при первом false

dropWhile() - пропускать пока условие true

Stream<T> dropWhile(Predicate<? super T> predicate)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 1, 2);

// Пропускать пока < 4, затем взять остальное
List<Integer> result = numbers.stream()
    .dropWhile(n -> n < 4)
    .toList();  // [4, 5, 1, 2]

Практический пример

// Логи с временными метками
List<LogEntry> logs = ...;

// Пропустить старые логи, взять начиная с определенного времени
LocalDateTime startTime = LocalDateTime.of(2024, 1, 1, 12, 0);

List<LogEntry> recentLogs = logs.stream()
    .dropWhile(log -> log.timestamp().isBefore(startTime))
    .toList();

mapMulti() (Java 16+)

Альтернатива flatMap() для более эффективного маппинга.

Сигнатура

<R> Stream<R> mapMulti(BiConsumer<T, Consumer<R>> mapper)

Пример

List<Integer> numbers = List.of(1, 2, 3);

// С flatMap
List<Integer> result = numbers.stream()
    .flatMap(n -> Stream.of(n, n * 2))
    .toList();  // [1, 2, 2, 4, 3, 6]

// С mapMulti (эффективнее - не создает промежуточные Stream)
List<Integer> result = numbers.stream()
    .<Integer>mapMulti((n, consumer) -> {
        consumer.accept(n);
        consumer.accept(n * 2);
    })
    .toList();  // [1, 2, 2, 4, 3, 6]

Когда использовать mapMulti

  • Когда из одного элемента нужно получить 0, 1 или несколько элементов
  • Когда создание Stream для каждого элемента неэффективно
  • Когда логика генерации сложная (условная)
// Фильтрация + преобразование в одной операции
List<Integer> result = objects.stream()
    .<Integer>mapMulti((obj, consumer) -> {
        if (obj instanceof String s) {
            consumer.accept(s.length());
        }
    })
    .toList();

Порядок операций имеет значение

Эффективный порядок

// Плохо: сначала сортировка всех, потом фильтрация
List<Person> result = people.stream()
    .sorted(Comparator.comparing(Person::age))  // Сортирует ВСЕ элементы
    .filter(p -> p.age() > 30)
    .limit(10)
    .toList();

// Хорошо: сначала фильтрация, потом сортировка меньшего набора
List<Person> result = people.stream()
    .filter(p -> p.age() > 30)  // Уменьшает набор
    .sorted(Comparator.comparing(Person::age))  // Сортирует только отфильтрованные
    .limit(10)
    .toList();

Правило оптимизации

1. filter() - уменьшить количество элементов
2. map() - преобразовать
3. sorted() - сортировать (уже уменьшенный набор)
4. limit() - ограничить

Комбинирование операций

Практический пример: обработка данных

record Transaction(String id, String type, double amount, LocalDate date) {}

List<Transaction> transactions = ...;

// Топ-5 крупных покупок за последний месяц
List<Transaction> result = transactions.stream()
    .filter(t -> t.type().equals("PURCHASE"))
    .filter(t -> t.date().isAfter(LocalDate.now().minusMonths(1)))
    .filter(t -> t.amount() > 1000)
    .sorted(Comparator.comparing(Transaction::amount).reversed())
    .limit(5)
    .toList();

Пример: обработка текста

String text = "  Hello   World!  This is   a   Test  ";

List<String> words = Arrays.stream(text.split("\\s+"))
    .map(String::trim)
    .filter(Predicate.not(String::isEmpty))
    .map(String::toLowerCase)
    .distinct()
    .sorted()
    .toList();  // [a, hello, is, test, this, world!]

Сводная таблица промежуточных операций

ОперацияОписаниеStateless/StatefulShort-circuit
filter()Фильтрация по условиюStatelessНет
map()Преобразование элементовStatelessНет
flatMap()Выравнивание вложенных структурStatelessНет
sorted()СортировкаStatefulНет
distinct()Удаление дубликатовStatefulНет
peek()Побочный эффект (отладка)StatelessНет
limit()Ограничить N элементамиStatefulДа
skip()Пропустить N элементовStatefulНет
takeWhile()Брать пока trueStatelessДа
dropWhile()Пропускать пока trueStatelessНет
mapMulti()Гибкий маппингStatelessНет

Итоги

  1. Промежуточные операции ленивые - выполняются только при терминальной операции
  2. filter() - отбирает элементы по условию
  3. map() - преобразует каждый элемент
  4. flatMap() - выравнивает вложенные структуры
  5. sorted() - сортирует (stateful, буферизует все элементы)
  6. distinct() - удаляет дубликаты (по equals/hashCode)
  7. peek() - для отладки, не для побочных эффектов
  8. limit()/skip() - для пагинации и ограничения
  9. Порядок операций важен - filter до sorted эффективнее

Задания для практики

  1. Фильтрация и преобразование: Дан список чисел от 1 до 100. Получи список квадратов всех четных чисел больше 50.

  2. flatMap: Дан список предложений. Получи список всех уникальных слов длиннее 3 символов, отсортированный по алфавиту.

  3. Пагинация: Реализуй метод paginate(List<T> items, int page, int size), возвращающий элементы указанной страницы.

  4. Сортировка объектов: Дан список Employee(name, department, salary). Отсортируй по департаменту, затем по зарплате (убывание).

  5. Комплексная задача: Дан список заказов Order(id, customerId, List<OrderItem>) где OrderItem(productName, quantity, price). Найди топ-3 клиента по общей сумме заказов.