Stream API

Вопросы к собеседованию

Вопросы:

Что такое Stream API?

Интерфейс java.util.Stream представляет собой последовательность элементов, над которой можно производить различные операции.

Нужны для упрощения работы с наборами данных, в частности, упростить операции фильтрации, сортировки и другие манипуляции с данными.


Создание экземпляра Stream

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

Стрим из BufferedReader с помощью метода lines(); нужно закрывать close().

подробнее


Типы операций

Промежуточные ("intermediate", "lazy")
Обрабатывают поступающие элементы и возвращают стрим.
Может быть много или ни одной.

Терминальные ("terminal", ещё называют "eager")
Обрабатывают элементы и завершают работу стрима.
Может быть только один.

Важные моменты:

Коллекции Streams
Конечны (хранят набор элементов) Бесконечны
Индивидуальный доступ к элементам Нет индивид. доступа к элементам
Можно менять (добавлять/удалять) элементы, в т.ч. через итератор Если как то обрабатываем данные, то не влияет на источник

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

Ленивое программирование - технология, которая позволяет вам отсрочить вычисление кода до тех пор, пока не понадобится его результирующее значение.

Блок обработки – промежуточные операции не выполняются, пока не вызовется терминальная.

Способы создания стрима

Из коллекции:

Stream<String> fromCollection = Arrays.asList("x", "y", "z").stream();

Из набора значений:

Stream<String> fromValues = Stream.of("x", "y", "z");

Из массива:

Stream<String> fromArray = Arrays.stream(new String[]{"x", "y", "z"});

Из файла (каждая строка в файле будет отдельным элементом в стриме):

Stream<String> fromFile = Files.lines(Paths.get("input.txt"));

Из строки:

IntStream fromString = "0123456789".chars();

С помощью Stream.builder():

Stream<String> fromBuilder = Stream.builder().add("z").add("y").add("z").build();

С помощью Stream.iterate() (бесконечный):

Stream<Integer> fromIterate = Stream.iterate(1, n -> n + 1);

С помощью Stream.generate() (бесконечный):

Stream<String> fromGenerate = Stream.generate(() -> "0");

Промежуточные методы

Метод Описание
peek()

Может видеть состояние данных в потоке (в основном для отладки)

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

map()

Позволяет задать функцию преобразования одного объекта в другой

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

flatMap()

Можно преобразовать один элемент в ноль, один или множество других

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

filter()

Который фильтрует все элементы, возвращая только те, что соответствуют условию

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

limit()

Возвращает модифицированный поток, в котором не более n элементов

Stream<T> limit(long maxSize);

skip()

Возвращает новый поток, в котором пропущены первые n элементов

Stream<T> skip(long n);

sorted()

Для сортировки тех объектов, которые реализуют интерфейс Comparable

Stream<T> sorted();

или

Для реализации своей логики сортировки

Stream<T> sorted(Comparator<? super T> comparator);

distinct()

Возвращает только уникальные элементы в виде потока

Stream<T> distinct();

Терминальные методы

Метод Описание
forEach()

Произведет переданное действие с каждым элементом стрима

void forEach(Consumer<? super T> action);

findFirst()

Первый в порядке следования элемент из стрима

Возвращает Optional т.к. элемента может не быть

Optional<T> findFirst();

allMatch()

Позволяет удостовериться, удовлетворяют ли все элементы стрима определенному условию

boolean allMatch(Predicate<? super T> predicate);

min()

Возвращает минимальный элемент из стрима

Optional<T> min(Comparator<? super T> comparator);

max()

Возвращает максимальный элемент из стрима

Optional<T> max(Comparator<? super T> comparator);

count()

Возвращает количество элементов, оставшееся в стриме

long count();

collect()

Собирает элементы стрима в новое хранилище (использует Collectors)

<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);

или

<R, A> R collect(Collector<? super T, A, R> collector);

reduce()

Позволяет выполнять какое-то действие на всей коллекции и возвращать один результат

T reduce(T identity, BinaryOperator<T> accumulator);

или

Optional<T> reduce(BinaryOperator<T> accumulator);

или

<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

Класс Collectors и его методы

в JAVA 8 в классе Collectors реализовано несколько распространённых коллекторов:

Метод Описание

toList()

toCollection()

toSet()

представляют стрим в виде списка, коллекции или множества

toConcurrentMap()

toMap()

позволяют преобразовать стрим в Map

averagingInt()

averagingDouble()

averagingLong()

возвращают среднее значение

summingInt()

summingDouble()

ummingLong()

возвращает сумму

summarizingInt()

summarizingDouble()

summarizingLong()

возвращают SummaryStatistics с разными агрегатными значениями
partitioningBy() разделяет коллекцию на две части по соответствию условию и возвращает их как Map<Boolean, List>
groupingBy() разделяет коллекцию на несколько частей и возвращает Map<N, List <T>>
mapping() дополнительные преобразования значений для сложных Collector-ов

Как создать свой коллектор?

Существует возможность создания собственного коллектора через Collector.of():

Collector <String, List<String>, List<String>> toList = Collector.of(
        ArrayList::new,
        List::add,
        (l1, l2) -> { l1.addAll(l2); return l1; }
    );

Параллельная обработке в JAVA 8

Стримы могут быть последовательными и параллельными.

Операции над последовательными стримами выполняются в одном потоке процессора, над параллельными — используя несколько потоков процессора.

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

Фактически применение параллельных стримов сводится к тому, что данные в стримах будут разделены на части, каждая часть обрабатывается на отдельном ядре процессора, и в конце эти части соединяются, и над ними выполняются конечные операции.

Для создания параллельного потока из коллекции можно также использовать метод parallelStream() интерфейса Collection.

Чтобы сделать обычный последовательный стрим параллельным, надо вызвать у объекта Stream метод parallel().

Метод isParallel() позволяет узнать является ли стрим параллельным.

С помощью, методов parallel() и sequential() можно определять какие операции могут быть параллельными, а какие только последовательными.

Так же из любого последовательного стрима можно сделать параллельный и наоборот:

collection
    .stream()
    .peek(...) // операция последовательна
    .parallel()
    .map(...) // операция может выполняться параллельно
    .sequential()
    .reduce(...) // операция снова последовательна

Как правило, элементы передаются в стрим в том же порядке, в котором они определены в источнике данных.
При работе с параллельными стримами система сохраняет порядок следования элементов.
Исключение составляет метод forEach(), который может выводить элементы в произвольном порядке. И чтобы сохранить порядок следования, необходимо применять метод forEachOrdered().


Критерии, которые могут повлиять на производительность в параллельных стримах:

Стримы для примитивов

Кроме универсальных объектных существуют особые виды стримов для работы с примитивными типами данных int, long и double: IntStream, LongStream и DoubleStream.

Эти примитивные стримы работают так же, как и обычные объектные, но со следующими отличиями:

Какие паттерны проектирования используется для реализации?

Stream API в Java использует несколько паттернов проектирования для реализации своей функциональности.

Основные из них:

Шаблон "Функциональный интерфейс"
Stream API во многом опирается на лямбда-выражения и функциональные интерфейсы. Функции, такие как map, filter, reduce, принимают в качестве параметров функциональные интерфейсы, что позволяет использовать лямбда-выражения для их реализации.

Шаблон "Итератор" (Iterator)
Stream API предоставляет возможность обрабатывать элементы потока последовательно, аналогично паттерну Итератор, но добавляет дополнительную гибкость благодаря ленивым операциям и параллельной обработке.

Шаблон "Строитель" (Builder)
Операции со стримами, такие как filter, map, sorted, комбинируются для создания конвейера обработки данных. Каждая из этих операций возвращает новый Stream, позволяя строить цепочку операций.

Шаблон "Декоратор"
Некоторые операции Stream API, такие как filter и map, можно рассматривать как декораторы, поскольку они оборачивают существующий поток в новый, добавляя дополнительную функциональность без изменения исходного объекта.

Шаблон "Команда" (Command)
Операции в Stream API, такие как forEach, reduce, выполняют действия, которые представляют собой команды, применяемые к элементам потока.