Stream API

Stream API #

1. Что такое Stream Api и для чего нужны Stream? #

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


С какими типами данных может работать?

  • ОбъектамиStream<T>, где T — любой ссылочный тип (например, String, Integer, пользовательские классы).

  • Примитивами — существуют специализированные стримы:

    • IntStream для работы с int.
    • LongStream для работы с long.
    • DoubleStream для работы с double.

    Для других примитивов стримы не предусмотрены, их нужно упаковывать в объекты-обёртки (например, Byte для byte).


Проблема с примитивами в стримах?

Работа с примитивами через Stream<T> приводит к автоупаковке и распаковке. Это накладывает следующие проблемы:

  1. Снижение производительности — упаковка/распаковка добавляют накладные расходы.
  2. Увеличение памяти — вместо примитивов создаются объекты (например, Integer, Double).

Специализированные стримы (IntStream, LongStream, DoubleStream) решают эти проблемы, обеспечивая работу непосредственно с примитивами без автоупаковки.


2. Почему Stream называют ленивым? #

Методы не будут выполняться, пока не будет вызван терминальный метод


3. Какие существуют способы создания Stream? #

  • Пустой стрим. Stream.empty()
  • Стрим из List. list.stream()
  • Стрим из Map. map.entrySet().stream()
  • Стрим из массива. Arrays.stream(array)
  • Стрим из указанных элементов. Stream.of(”1”, “2”, “3”)
  • Конкатенацией двух стримов

4. Типы методов в Stream API #

Промежуточные и терминальные.

Что такое промежуточные методы? Какие промежуточные методы в стримах вы знаете?

Промежуточный метод - метод, который возвращает Stream. Существуют:

  • filter(Predicate<T> predicate) Используется для фильтрации элементов потока по условию
List<String> names = List.of("hey", "goodbye", "ok");

List<String> newNames = names.stream()
                .filter(s -> s.length() > 2)
                .toList();
  • map(Function<T, R> mapper) Преобразует каждый элемент потока в другой элемент
List<Integer> numbers = List.of(1,2,3,4);
        numbers.stream()
                .map(i -> i * i)
                .forEach(System.out::println);
  • flatMap(Function<T, Stream<R>> mapper) Разворачивает вложенные структуры, объединяя несколько потоков в один
List<Human> humans = asList(
                new Human("Sam", asList("Buddy", "Lucy")),
                new Human("Bob", asList("Frankie", "Rosie")),
                new Human("Marta", asList("Simba", "Tilly")));

        List<String> petNames = humans.stream()
                .flatMap(human -> human.getPetNames().stream())
                .toList();
                // Вернется список имен домашних животных (Buddy, Lucy и т.д)
  • limit(long maxSize) Ограничивает количество элементов в потоке
List<String> names = List.of("hey", "goodbye", "ok");

        List<String> newNames = names.stream()
                .limit(2)
                .toList();
                // Будет выведено два первых слова
  • skip(long n) Пропускает первые n элементов в потоке
List<String> names = List.of("hey", "goodbye", "ok");

        List<String> newNames = names.stream()
                .skip(2)
                .toList();
                // Будет выведена последняя строка 'ok'
  • concat(Stream<T> a, Stream<T> b) Объединяет два потока в один
List<String> listOne = List.of("Apple", "Banana", "Cherry");
        List<String> listTwo = List.of("Orange", "Peach", "Plum");

        Stream<String> combinedStream = Stream.concat(listOne.stream(), listTwo.stream());
        combinedStream.forEach(System.out::println);
  • peek(Consumer<T> action) Выполняет действие над каждым элементом потока (например, для отладки)
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

        List<Integer> result = numbers.stream()
                .peek(num -> System.out.println("Исходное число:" + num))
                .map(num -> num * 2)
                .peek(num -> System.out.println("Число после удвоения" + num))
                .toList();
                // Вывод:
                // Исходное число: 1
                // Число после удвоения: 2 и т.д
  • distinct() Убирает дубликаты, оставляя только уникальные элементы
List<Integer> numbers = asList(1,2,3,4,5,5);

        List<Integer> distinctNumbers = numbers.stream()
                .distinct()
                .toList();
  • sorted() / sorted(Comparator<T> comparator) Использует естественный порядок фильтрации, либо принимает Comparator для сортировки элементов
List<String> words = List.of("hello", "bye", "ok", "goodbye", "andrew");

        List<String> sortedWords = words.stream()
                .sorted(Comparator.comparing(String::length))
                .toList();

Что такое терминальные методы? Какие терминальные методы в стримах вы знаете?

Терминальные методы завершают работу с потоком, после их вызова дальнейшие операции невозможны. Основные терминальные методы:

  • forEach(Consumer<T> action) Выполняет действие над каждым элементом потока
List<String> words = List.of("hi", "hi", "goodbye", "ok", "dog", "cat", "dog");

        words.stream()
                .filter(w -> w.length() > 2)
                .forEach(System.out::println);
  • collect(Collector<T, A, R> collector) Преобразует элементы потока в другую структуру данных (список, множество и т.д.)
List<String> words = List.of("hi", "hi", "goodbye", "ok", "dog", "cat", "dog");

        Set<String> wordsSet = words.stream()
                .filter(w -> w.length() > 2)
                .collect(Collectors.toSet());
  • reduce(BinaryOperator<T> accumulator) Сворачивает все элементы потока в одно значение, используя бинарную операцию
List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        int sum = numbers.stream()
                .reduce(0, Integer::sum);
  • count() Возвращает количество элементов в потоке
List<Integer> numbers = List.of(1, 2, 3, 4, 10, 1298, 27);

        long counted = numbers.stream()
                .filter(n -> n % 2 == 0)
                .count(); // Посчитали четные числа
  • min(Comparator<T> comparator) Возвращает минимальный элемент потока по заданному компаратору
List<Integer> numbers = List.of(1, 2, 3, 4, 10, 1298, 27);

        int max = numbers.stream()
                .min(Integer::compareTo).orElseThrow();
  • max(Comparator<T> comparator) Возвращает максимальный элемент потока по заданному компаратору
List<Integer> numbers = List.of(1, 2, 3, 4, 10, 1298, 27);

        int max = numbers.stream()
                .max(Integer::compareTo).orElseThrow();
  • findFirst() Возвращает первый элемент потока (опционально)
List<Integer> numbers = List.of(1,2,3,4,5,6,10,120,130);

        numbers.stream()
                .filter(n -> n > 10)
                .findFirst()
                .orElseThrow();
  • findAny() Возвращает любой элемент потока (опционально, полезно в параллельных потоках)
List<String> names = List.of("hello java", "goodBye");

        Optional<String> name = names.stream()
                .filter(s -> s.contains("java"))
                .findAny();
  • anyMatch(Predicate<T> predicate) Возвращает true, если хотя бы один элемент соответствует условию
List<Integer> nums = List.of(1,2,3,4,5,6);

        boolean is = nums.stream()
                .anyMatch(n -> n % 2 == 0);
                //Вернет true
  • allMatch(Predicate<T> predicate) Возвращает true, если все элементы соответствуют условию
List<Integer> nums = List.of(-1,2,3,4,5,6);

        boolean is = nums.stream()
                .allMatch(n -> n > 0);
                //Вернет false
  • noneMatch(Predicate<T> predicate) Возвращает true, если ни один элемент не соответствует условию
List<Integer> nums = List.of(1,3,3,5,5,7);

        boolean is = nums.stream()
                .noneMatch(n -> n % 2 == 0);
                //Вернет true (т.к ни одно не удовлетворяет условию)

Можно ли переиспользовать Stream после терминального метода?

Нет, стрим нельзя переиспользовать после вызова терминального метода. После выполнения терминальной операции стрим закрывается, и любая последующая попытка вызова методов вызовет IllegalStateException.

Если нужно работать с данными снова, их придётся пересоздать (например, вызвать .stream() для исходной коллекции).


Использование Stream без терминального метода?

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


5. Методы в Stream API #

Метод peek()

Метод peek() используется для промежуточной обработки элементов стрима. Он позволяет выполнить действие над каждым элементом стрима без изменения самих элементов. Основное применение — это отладка или логирование данных перед выполнением конечной операции.

Важно: метод peek() не изменяет поток и чаще всего используется для побочных эффектов (например, вывода информации о каждом элементе).

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = numbers.stream()
    .peek(n -> System.out.println("Processing: " + n))
    .map(n -> n * n)
    .collect(Collectors.toList());
System.out.println(result); // [1, 4, 9, 16, 25]

Метод map()

Метод map() применяется для преобразования каждого элемента стрима с помощью функции. Он берет входной элемент и возвращает новый элемент, который соответствует результату примененной функции.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
    .map(n -> n * n)
    .collect(Collectors.toList());
System.out.println(squares); // [1, 4, 9, 16, 25]

Метод flatMap()

Метод flatMap() используется для преобразования каждого элемента стрима в другой стрим, а затем “выравнивания” полученных стримов в один плоский стрим. Это особенно полезно для работы с коллекциями внутри коллекций.

List<List<Integer>> numbers = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4),
    Arrays.asList(5)
);
List<Integer> flatNumbers = numbers.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
System.out.println(flatNumbers); // [1, 2, 3, 4, 5]

Чем отличаются методы map() и flatMap()?

  • map() преобразует каждый элемент в один элемент (или объект).
  • flatMap() преобразует каждый элемент в стрим и объединяет все полученные стримы в один.

Метод filter()

Метод filter() используется для фильтрации элементов стрима на основе условия (предиката). Он пропускает элементы, которые не удовлетворяют условию.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4]

Метод limit()

Метод limit() возвращает стрим, состоящий не более чем из указанного количества элементов. Это полезно для ограничения размера стрима.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> limited = numbers.stream()
    .limit(3)
    .collect(Collectors.toList());
System.out.println(limited); // [1, 2, 3]

Метод skip()

Метод skip() пропускает указанное количество элементов и возвращает стрим, начинающийся с оставшихся элементов.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> skipped = numbers.stream()
    .skip(2)
    .collect(Collectors.toList());
System.out.println(skipped); // [3, 4, 5]

Метод sorted()

Метод sorted() сортирует элементы стрима. По умолчанию элементы сортируются в естественном порядке, но можно задать компаратор для пользовательской сортировки.

List<String> names = Arrays.asList("John", "Anna", "Tom");
List<String> sortedNames = names.stream()
    .sorted()
    .collect(Collectors.toList());
System.out.println(sortedNames); // [Anna, John, Tom]

Метод distinct(). Сложность по времени

Метод distinct() возвращает стрим, который содержит только уникальные элементы (без дубликатов).

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5);
List<Integer> uniqueNumbers = numbers.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(uniqueNumbers); // [1, 2, 3, 4, 5]

Сложность по времени

  • Добавление в LinkedHashSet:

    • Каждая операция добавления имеет амортизированную сложность O(1) при удачном хэшировании.
    • В худшем случае (много коллизий) — O(n) на элемент.
  • Общая сложность метода:

    • При удачном хэшировании: O(n), где n — количество элементов в исходном потоке.
    • При плохом хэшировании (много коллизий): до O(n^2).

    В реальных сценариях при корректно реализованных hashCode() и equals() метод работает с линейной сложностью O(n).


Метод collect()

Метод collect() используется для преобразования стрима в коллекцию или другой тип данных. Чаще всего используется для сбора элементов в список, множество или строку.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> collectedList = numbers.stream()
    .collect(Collectors.toList());
System.out.println(collectedList); // [1, 2, 3, 4, 5]

Метод groupingBy()

Разделяет элементы исходной коллекции или потока на группы (категории) на основе указанной функции.

Возвращает результат в виде Map, где:

  • Ключ — значение, возвращённое функцией группировки.
  • Значение — список элементов, соответствующих этому ключу.
List<String> strings = Arrays.asList("cat", "dog", "fish", "ant", "elephant");

        Map<Integer, List<String>> groupedByLength = strings.stream()
                .collect(Collectors.groupingBy(String::length));

        System.out.println(groupedByLength);

  //3=[cat, dog, ant], 
  //4=[fish], 
  //8=[elephant]

Метод reduce()

Метод reduce() сводит (агрегирует) элементы стрима к одному значению с помощью функции аккумулятора. Например, для вычисления суммы или произведения всех элементов.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .reduce(0, Integer::sum); // 0 — начальное значение
System.out.println(sum); // 15

Метод parallelStream()

Метод parallelStream() создаёт стрим, выполняющий операции в несколько потоков (параллельно). Он используется для коллекций и предоставляет возможность автоматически разделить обработку данных на части.

import java.util.Arrays;
import java.util.List;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", "B", "C", "D");

        list.parallelStream()
            .forEach(item -> {
                System.out.println(Thread.currentThread().getName() + " processes " + item);
            });
    }
}

6. Расскажите про класс Collectors и его методы #

Класс Collectors предоставляет ряд статических методов для создания коллекций, таких как списки, множества, карты и другие, из элементов потока (Stream). Эти методы широко используются для агрегации данных в потоках. Основные методы:

  • toList() Собирает элементы потока в список
  • toSet() Собирает элементы потока в множество (Set)
  • toMap() Собирает элементы потока в карту (Map), используя ключи и значения, которые задаются функциями
  • joining() Объединяет элементы потока в одну строку, используя заданный разделитель и префикс/суффикс
  • counting() Считает количество элементов в потоке
  • summarizingInt() Собирает статистику по числовым значениям (например, сумма, среднее, максимальное и минимальное значение)
  • averagingInt() Вычисляет среднее значение по числовым элементам
  • partitioningBy() Разделяет элементы потока на две группы по заданному предикату
  • groupingBy() Группирует элементы потока по заданному классификатору
  • reducing() Выполняет редукцию элементов потока с использованием заданного бинарного оператора
  • toCollection(Supplier<C>) Превращает поток в коллекцию
List<String> names = Arrays.asList("Jaime", "", "Tyron");

        Queue<String> newNames = names.stream()
                .filter(s -> !s.isEmpty())
                .collect(Collectors.toCollection(LinkedList::new));

Для группировки элементов в Map какой Collector будешь использовать?


7. Что такое IntStream и DoubleStream? #

IntStream и DoubleStream - это специальные стримы в Java для работы с примитивами int и double. Поддерживают дополнительные терминальные методы:

  • sum()
  • average()
  • mapToObj()

8. Разница между parallel и parallerStream? #

МетодОписание
stream().parallel()Преобразует уже существующий стрим в параллельный.
parallelStream()Создаёт сразу параллельный стрим из коллекции.

Оба метода дают один и тот же результат, но разница в способе вызова:

  • list.stream().parallel() полезно, если сначала требуется настроить стрим (например, добавить фильтрацию), а затем переключить его в параллельный режим.
  • list.parallelStream() используется для простоты, если сразу нужен параллельный стрим.

9. Для чего нужны операции Consumer, Function, Supplier #

В Java 8 появились функциональные интерфейсы из пакета java.util.function, которые используются в лямбда-выражениях и Stream API. Среди них важны:

Функциональный интерфейсВходные данныеВозвращаемое значениеПрименение
Consumer<T>Принимает TНичего не возвращает (void)Используется, когда нужно выполнить действие, но не вернуть результат.
Function<T, R>Принимает TВозвращает RПреобразует один тип данных в другой.
Supplier<T>Не принимает аргументовВозвращает TИспользуется для генерации значений.

Consumer<T> — потребитель

Consumer<T> используется, когда нужно выполнить операцию над объектом, но ничего не возвращать.

Пример: печать списка элементов

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class ConsumerExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        Consumer<String> printName = name -> System.out.println("Hello, " + name);
        names.forEach(printName);
    }
}

Вывод:

Hello, Alice
Hello, Bob
Hello, Charlie

💡 Где используется?

  • В forEach() коллекций
  • Логирование (Logger::info)
  • Работа с файлами (Files.lines().forEach(...))

Function<T, R> — преобразователь

Function<T, R> принимает объект типа T и возвращает объект типа R.

Пример: преобразование строки в ее длину

import java.util.function.Function;

public class FunctionExample {
    public static void main(String[] args) {
        Function<String, Integer> lengthFunction = str -> str.length();

        System.out.println(lengthFunction.apply("Java")); // 4
        System.out.println(lengthFunction.apply("Functional Interfaces")); // 21
    }
}

💡 Где используется?

  • Преобразование данных в Stream API
  • Маппинг объектов (List<String>List<Integer>)
  • Фильтрация и сортировка

Supplier<T> — поставщик

Supplier<T> ничего не принимает, но генерирует результат.

Пример: генерация случайного числа

import java.util.function.Supplier;
import java.util.Random;

public class SupplierExample {
    public static void main(String[] args) {
        Supplier<Integer> randomSupplier = () -> new Random().nextInt(100);

        System.out.println(randomSupplier.get()); // Например, 42
        System.out.println(randomSupplier.get()); // Например, 87
    }
}

💡 Где используется?

  • Ленивая инициализация
  • Генерация случайных данных
  • Фабричные методы

10. Что такое параллельные стримы? #

Параллельные стримы позволяют разделить обработку данных на несколько потоков, используя ForkJoinPool. Это ускоряет обработку больших объемов данных.

Пример 1: Сравнение stream() и parallelStream()

import java.util.Arrays;
import java.util.List;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave");

        // Последовательный стрим
        System.out.println("Sequential Stream:");
        names.stream().forEach(name -> System.out.println(Thread.currentThread().getName() + ": " + name));

        // Параллельный стрим
        System.out.println("\nParallel Stream:");
        names.parallelStream().forEach(name -> System.out.println(Thread.currentThread().getName() + ": " + name));
    }
}

Вывод (примерный):

Sequential Stream:
main: Alice
main: Bob
main: Charlie
main: Dave

Parallel Stream:
ForkJoinPool.commonPool-worker-1: Alice
ForkJoinPool.commonPool-worker-3: Bob
ForkJoinPool.commonPool-worker-2: Charlie
main: Dave

💡 Видно, что данные обрабатываются разными потоками в parallelStream()!

Когда использовать параллельные стримы?

Когда данных очень много (миллионы записей).
Когда операции CPU-интенсивные (например, сложные вычисления).
Когда нет зависимостей между элементами (например, суммирование чисел).

🚫 Когда НЕ использовать?
❌ Если коллекция маленькая (параллельность создаст накладные расходы).
❌ Если порядок важен (параллельные стримы могут изменить порядок).
❌ Если есть побочные эффекты (forEach() в parallelStream может работать непредсказуемо).

Пример 2: Ускорение вычислений

Пример суммирования чисел с использованием параллельного стрима:

import java.util.stream.LongStream;

public class ParallelStreamSum {
    public static void main(String[] args) {
        long startTime, endTime;

        // Последовательное суммирование
        startTime = System.nanoTime();
        long sum1 = LongStream.rangeClosed(1, 10_000_000).sum();
        endTime = System.nanoTime();
        System.out.println("Sequential sum: " + sum1 + ", time: " + (endTime - startTime) / 1_000_000 + " ms");

        // Параллельное суммирование
        startTime = System.nanoTime();
        long sum2 = LongStream.rangeClosed(1, 10_000_000).parallel().sum();
        endTime = System.nanoTime();
        System.out.println("Parallel sum: " + sum2 + ", time: " + (endTime - startTime) / 1_000_000 + " ms");
    }
}

Вывод:

Sequential sum: 50000005000000, time: 30 ms
Parallel sum: 50000005000000, time: 7 ms

Параллельный стрим в 4 раза быстрее!


11. Какая разница между findAny() и findFirst() #

КритерийfindAny()findFirst()
Что возвращаетЛюбой элемент потока (если есть)Первый элемент потока (если есть)
ПорядокНе гарантирует порядокГарантирует, что вернёт именно первый элемент в порядке стрима
Параллельный стримМожет работать быстрее (не нужно соблюдать порядок)Может работать медленнее (должен сохранять порядок)
Когда использоватьЕсли неважно, какой элемент получитьЕсли нужно получить именно первый элемент в порядке
Примерstream.parallel().findAny()stream.parallel().findFirst()

Итог:

  • Если важен порядок — используй findFirst().
  • Если порядок не важен, и нужна производительностьfindAny().