Stream API. Способы создания Stream'а

Stream API. Способы создания Stream'а


В рамках прошлого урока мы разобрались, на базе каких механизмов работает Stream API. Однако эта информация не дает полноценного ответа на вопрос «как создать Stream?» в рамках прикладных задач. Текущий урок будет посвящен именно этому.

 

Самые простые решения – самые верные

Начнем с очевидного. Многие классы, так или иначе связанные с обработкой массивов данных, имеют методы, возвращающие Stream.

Это могут как классы, предоставляемые Java, так и классы внешних библиотек или даже классы разрабатываемого вами проекта. В данном случае это не имеет значения. Если класс работает с какими-то элементами и имеет методы, возвращающие их как объект типа Stream – в 99% случаев именно им и стоит воспользоваться. То же правило применимо и к Optional.

Отсюда следует, что при работе с незнакомым вам классом, в первую очередь следует ознакомиться с методами, которые у него есть. Если ваша проблема выглядит популярной – вполне вероятно, что существует не менее популярное решение для нее. В т.ч. предоставленное разработчиками класса, с которым вы работаете.

 

StreamSupport

StreamSupport утилитный класс (класс, содержащий только статические методы), который предлагает инструменты для создания Stream’ов из сплитератора.

Предоставляет два основных метода:

·      stream(Spliterator<T> spliterator, boolean parallel);

Параметрами принимает сплитератор и boolean-флаг, в зависимости от значения которого Stream будет обрабатываться в одном или нескольких потоках. Но с этим мы будем разбираться подробнее в теме многопоточности;
Данный метод является самым популярным в данном классе. Например, именно его используют все основные Java-коллекции внутри собственных методов stream() и parallelStream().
Важно: характеристики сплитератора, в данном случае, передаются и Stream’у, который будет создан.

·      stream(Supplier<Spliterator<T>> supplier, int characteristics, boolean parallel).

Данный метод отличается от предыдущего двумя особенностями:
·      Он не принимает объект сплитератора, вместо этого передается лямбда-выражение, результатом которого является сплитератор. Это имеет смысл, когда на базе однотипного сплитератора нужно создать несколько Stream’ов. Один сплитератор нельзя обработать дважды, соответственно, придется либо дублировать код по созданию объекта сплитератора, либо записать этот код в лямбда-выражение и использовать уже его. Второй вариант лаконичнее и удобнее в дальнейшей поддержке.
·      Характеристики Stream’а передаются отдельным параметром, а не берутся из характеристик сплитератора.

Кроме двух описанных выше методов, класс StreamSupport имеет еще 6 для создания Stream’ов на базе примитивов - int, long, double (по 2 метода на каждый). Параметры эти методов подобны описанным выше. Зачем это нужно и в чем отличия IntStream, LongStream и DoubleStream от Stream – разберемся в отдельном уроке.

Отдельно напомню, что для создания Stream не обязательно иметь сплитератор – достаточно итератора. Сплитератор из итератора можно создать, например, используя класс Spliterators.
Он же подойдет для создания сплитератора на базе массива или коллекции (если вам, по какой-то причине не подошел дефолтный сплитератор, предоставляемый этой коллекцией). Но этими возможностями вам вряд ли придется когда-либо пользоваться.

 

Stream’ы на базе массивов. Класс Arrays

Класс Arrays является утилитным классом, предлагающим массу разнообразных инструментов для работы с массивами, включая методы для сортировки, поиска, копирования и т.д.
В числе прочего данный класс имеет методы для создания сплитераторов и стримов на базе массива или его части.
Методы для создания сплитераторов вы можете рассмотреть самостоятельно, а вот надстройку над ними – методы для создания стрима – разберем в статье:

1.    stream(T[] array). Создает Stream на базе массива произвольного типа. Перегружен также для int[], double[] и long[];

Отсюда следует, что сделать Stream на базе, скажем, float[] или другого массива примитивов, не получится. Как это можно обойти – рассмотрим в одном из пунктов ниже.

2.    stream(T[] array, int startInclusive, int endExclusive). Создает Stream на базе части массива. От элемента с индексом startInclusive, до элемента (не включая) с индексом endExclusive. Также перегружен для int[], double[] и long[].


На практике создавать Stream из массива приходится не часто. Когда же приходится – как правило, это создание Stream’а над массивом ссылочного типа. Что, в целом, оправдывает отсутствие методов для создания стрима из массива большей части примитивных типов – в них просто нет необходимости, исключая какие-то совсем редкие ситуации.

 

Методы-источники в классе Stream

Для ситуаций, когда способы выше не подошли, а Stream создать нужно, существует ряд статических методов в интерфейсе Stream:

1.    empty(). Создает пустой Stream. Используется, например, в методе stream() у класса Optional и в качестве заглушки у методов, возвращающих Stream. Область применения примерно та же, что и у пустой immutable-коллекции;

2.    ofNullable(). Принимает объект любого типа. Если передан null – будет создан пустой Stream, в иных случаях – Stream из одного (переданного параметром) элемента;

3.    of() (для одного элемента). Создает Stream из одного (переданного параметром) элемента. Но если параметром передать null – бросит NPE;

4.    of() (varargs). Принимает любой набор значений, на базе которых создает Stream. В отличии от реализации для одного параметра, может принимать null. Но только в случае, если число аргументов >1;

5.    generate(). Принимает параметром лямбда-выражение. Создает бесконечный Stream, каждый элемент которого – результат нового вызова лямбда-выражения. Обычно используется вместе с методами, ограничивающими число элементов в Stream’е. Например, limit() или takeWhile();

6.    iterate() (без ограничений). Принимает в себя значение, которое будет первым элементом стрима, и лямбда-выражение, которое на базе предыдущего значения генерирует новое. Создает бесконечный Stream. Например:

Stream.iterate(1, i -> ++i)
  .limit(10)
  .forEach(i -> System.out.print(i)); //12345678910
Обратите внимание: если вы не создали (явно или нет) новый объект в лямбда-выражении,  а возвращаете тот, который приняли параметром – вы создадите Stream, каждый элемент которого ссылается на один и тот же объект.

7.    iterate() (c ограничением). Представляет собой некий аналог цикла for. Кроме параметров, описанных в п.6, имеет параметр-предикат, который проверяет, стоит ли прервать генерацию. Например:

Stream.iterate(1, i -> i <= 10, i -> ++i)
  .forEach(i -> System.out.print(i)); //12345678910

8.    concat(). Принимает параметрами два стрима и объединяет их в один;

9.    builder(). Возвращает объект типа Stream.Builder, позволяющего сформировать Stream. Непопулярен, поэтому не вижу смысла описывать его в данной статье. Отмечу лишь, что данный инструмент является реализацией паттерна проектирования Builder (Строитель). Ознакомиться с примерами использования Stream.Builder можно по ссылкам:
https://hr-vector.com/java/metod-build

https://hr-vector.com/java/metod-accept-stream-builder


Данные методы покрывают обширную область прикладных задач. Именно им мы посвятим сегодняшнюю практику – все остальное, на мой взгляд слишком очевидно.

 

Методы создания Stream в IntStream и LongStream

Кроме интерфейса Stream, существует 3 интерфейса Stream API, предназначенных для работы с примитивами. Мы их уже упоминали в пункте о StreamSupport: IntStream, LongStream и DoubleStream.

Последний – DoubleStream – нас не интересует, поскольку не имеет уникальных методов-источников, лишь аналоги тех, которые представлены в Stream.

Зато IntStream и LongStream предлагают то, чего у Stream нет:

·      static range(int startInclusive, int endExclusive). Для LongStream параметры будут типа long;

Метод создаст стрим – IntStream или LongStream, в зависимости от того, у какого интерфейса вызван, наполненный элементами от startInclusive до endExclusive – 1.
С поправкой на интерфейс, идентичен записи:
Stream.iterate(startInclusive, i -> i < endExclusive, ++i);
//ограничение - i МЕНЬШЕ endExclusive

·      static rangeClosed(int startInclusive, int endInclusive). Для LongStream параметры будут типа long.

Является копией предыдущего метода, только значение второго параметра попадет в стрим. Идентичная запись для Stream:
Stream.iterate(startInclusive, i -> i <= endInclusive, ++i);
//ограничение - i МЕНЬШЕ ИЛИ РАВНО endInclusive


Бонус

Как вы помните, мы не можем создать Stream из массива типа float. Но мы можем сделать некоторую обертку, которая позволит на базе float[] создать Stream<Float>. Все то же самое актуально и для остальных примитивных типов.

Вариант 1:

float[] arr = new float[]{11f, 2f, 4f};

IntStream.range(0, arr.length)
  .mapToObj(i -> arr[i]) //получили Stream<Float>
  …

Вариант 2:

float[] arr = new float[]{11f, 2f, 4f};

Stream.iterate(0, i -> i < arr.length, i -> ++i)
  .map(i -> arr[i]) //получили Stream<Float>
  …


С теорией на сегодня все!


Переходим к практике:

Задача 1

Реализуйте задачу 5.1 из урока 4, используя Stream API.

Подсказка: https://pastebin.com/BnrsBRaV


Задача 2

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

02.02.2023

... 

28.02.2023

Вариант 1: используя limit();

Вариант 2: НЕ используя limit().


Задача 3

Реализуйте метод, возвращающий Stream<String> из дат месяца, номер которого был передан параметром. Выведите все даты года в консоль. Избегайте дублирования кода.

Вариант 1: каждый Stream, возвращенный из метода, должен быть сохранен в отдельную переменную. Подсказка:  https://pastebin.com/rJ3uRWC1

Вариант 2(*): в main() Stream должен быть лишь 1. Требует использования flatMap().


Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)

Канал: https://t.me/+relA0-qlUYAxZjI6

Мой тг: https://t.me/ironicMotherfucker

 

Дорогу осилит идущий!

Report Page