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/Stateful | Short-circuit |
|---|---|---|---|
filter() | Фильтрация по условию | Stateless | Нет |
map() | Преобразование элементов | Stateless | Нет |
flatMap() | Выравнивание вложенных структур | Stateless | Нет |
sorted() | Сортировка | Stateful | Нет |
distinct() | Удаление дубликатов | Stateful | Нет |
peek() | Побочный эффект (отладка) | Stateless | Нет |
limit() | Ограничить N элементами | Stateful | Да |
skip() | Пропустить N элементов | Stateful | Нет |
takeWhile() | Брать пока true | Stateless | Да |
dropWhile() | Пропускать пока true | Stateless | Нет |
mapMulti() | Гибкий маппинг | Stateless | Нет |
Итоги
- Промежуточные операции ленивые - выполняются только при терминальной операции
- filter() - отбирает элементы по условию
- map() - преобразует каждый элемент
- flatMap() - выравнивает вложенные структуры
- sorted() - сортирует (stateful, буферизует все элементы)
- distinct() - удаляет дубликаты (по equals/hashCode)
- peek() - для отладки, не для побочных эффектов
- limit()/skip() - для пагинации и ограничения
- Порядок операций важен - filter до sorted эффективнее
Задания для практики
-
Фильтрация и преобразование: Дан список чисел от 1 до 100. Получи список квадратов всех четных чисел больше 50.
-
flatMap: Дан список предложений. Получи список всех уникальных слов длиннее 3 символов, отсортированный по алфавиту.
-
Пагинация: Реализуй метод
paginate(List<T> items, int page, int size), возвращающий элементы указанной страницы. -
Сортировка объектов: Дан список
Employee(name, department, salary). Отсортируй по департаменту, затем по зарплате (убывание). -
Комплексная задача: Дан список заказов
Order(id, customerId, List<OrderItem>)гдеOrderItem(productName, quantity, price). Найди топ-3 клиента по общей сумме заказов.