Java. Интерфейс Stream . Промежуточные методы интерфейса
Интерфейс Stream<T> . Промежуточные методы интерфейса. Примеры использования. Методы filter() , map() , mapToDouble() , mapToInt() , mapToLong() , sorted()
Содержание
- 1. Понятие промежуточной операции. Промежуточные операции без сохранения состояния и с сохранением состояния
- 2. Метод filter() . Получить новый поток согласно условию (предикату). Пример
- 3. Метод map() . Применить указанную функцию отображения к элементам вызывающего потока. Пример
- 4. Метод mapToDouble() . Применить функцию отображения к потоку данных типа DoubleStream
- 5. Метод mapToInt() . Применить функцию отображения к потоку данных типа IntStream . Пример
- 6. Метод mapToLong() . Применить функцию отображения к потоку данных типа LongStream . Пример
- 7. Метод sorted() . Отсортировать элементы в порядке возрастания и убывания. Пример
Поиск на других ресурсах:
1. Понятие промежуточной операции. Промежуточные операции без сохранения состояния и с сохранением состояния
Под промежуточной операцией имеется ввиду метод (функция), которая создает новый поток данных на основании исходного (вызывающего) потока данных. Создание нового потока данных может происходить в несколько этапов в режиме конвейера.
Для промежуточных операций характерно:
- выполнение операции без сохранения состояния. В этом случае каждый элемент обрабатывается независимо от других. Например, в методе filter() выборка элемента в новый поток не зависит от значений других элементов исходного потока;
- выполнение операции с сохранением состояния. В этом случае обработка элемента зависит от особенностей других элементов. Примером этому может служить сортировка элементов, где позиция элемента зависит от значений других элементов (метод sorted() ).
При выполнении промежуточных операций состояние сохранения (не сохранения) является важным в случае параллельной обработки потоков данных. Это связано с тем, что операции с сохранением состояния могут быть выполнены за несколько проходов. В то же время, операции без сохранения состояния могут быть выполнены за один проход.
2. Метод filter() . Получить новый поток согласно условию (предикату). Пример
Метод filter() предназначен для получения нового потока согласно заданному условию. Метод имеет следующую общую форму
- T – тип элементов потока;
- Stream<T> – результирующий отфильтрованный поток;
- Predicate<? super T> – предикат, определяющий условие фильтрации.
Метод filter() есть промежуточной операцией (intermediate operation).
Пример. В примере метод filter() используется для создания нового потока чисел типа Double. В новом потоке формируются числа, больше значения 2.5.
Результат выполнения программы
3. Метод map() . Применить указанную функцию отображения к элементам вызывающего потока. Пример
Метод map() используется для применения к потоку некоторой функции отображения. Метод возвращает результирующий поток. Метод имеет следующую общую форму:
- R – тип элементов результирующего потока;
- T – тип элементов исходного потока;
- mapper – лямбда-выражение, которое выполняет действие над элементами исходного потока;
- Function<? super T, ? extends R> – тип, соответствующий стандартному функциональному интерфейсу Java. В этом интерфейсе определен метод apply() , который выполняет некоторую функцию над элементами потока.
Метод есть промежуточной операцией (intermediate operation).
Пример. В примере демонстрируется применение метода map() для следующих задач:
- для заданного потока строк сформировать новый поток целых чисел. Каждое число результирующего потока является длиной соответствующей строки исходного потока;
- для заданного потока строк сформировать новый поток строк. Каждая строка результирующего потока является реверсивной (обернутой) к соответствующей строке исходного потока.
Результат выполнения программы
4. Метод mapToDouble() . Применить функцию отображения к потоку данных типа DoubleStream
Метод mapToDouble() предназначен для применения функции отображения к потоку данных типа Double . Общая форма метода следующая:
- T – тип элементов исходного потока данных;
- DoubleStream – последовательность элементов примитивного типа double , которая есть специализацией потока Stream ;
- ToDoubleFunction<? super T> – функциональный интерфейс, содержащий метод applyAsDouble() . Метод получает данные обобщенного типа T и возвращает результат типа double . Это специализация функционального интерфейса Function , которая возвращает тип double .
Метод mapToDouble() является промежуточным операцией.
Пример. В примере метод mapToDouble() используется для получения потока данных типа Double из потока данных типа Integer . Над каждым элементом входного потока данных типа Integer выполняется операция взятия корня квадратного.
Результат выполнения программы
5. Метод mapToInt() . Применить функцию отображения к потоку данных типа IntStream . Пример
Метод mapToInt() предназначен для применения функции отображения к потоку данных типа Integer . Общая форма метода следующая
- IntStream – последовательность значений типа int , которая является примитивной int-реализацией типа Stream ;
- ToIntFunction<? super T> – тип функции отображения, которая конвертирует данные обобщенного типа T в данные типа int . Это специализация стандартного интерфейса Function для типа int ;
- mapper — функция отображения, которая применяется к потоку данных обобщенного типа T .
Пример. В примере исходный поток типа Double конвертируется в результирующий поток типа Integer . Каждый элемент исходного потока округляется к ближайшему целому.
Результат выполнения программы
6. Метод mapToLong() . Применить функцию отображения к потоку данных типа LongStream . Пример
Метод mapToLong() используется в случаях, когда нужно получить поток данных типа Long . В методе для некоторого типа T применяется функция отображения, которая возвращает результат типа Long . Общая форма метода следующая:
- mapper – функция отображения, которая представлена в виде лямбда-выражения. Функция получает значение элемента обобщенного типа T и возвращает результат;
- ToLongFunction<? super T> – тип функции отображения, предназначенной для получения данных типа Long ;
- LongStream – тип, отражающий последовательность элементов типа long .
Метод mapToLong() есть промежуточной операцией.
Пример. В примере числа типа Integer исходного потока конвертируются в результирующий поток типа Long . Каждое число потока возводится в квадрат.
Результат выполнения программы
7. Метод sorted() . Отсортировать элементы в порядке возрастания и убывания. Пример
Метод sorted() позволяет получить отсортированный поток из вызывающего потока. Метод имеет две перегруженные реализации.
Общая форма первой реализации метода следующая:
- T – тип элементов потока;
- Stream<T> – результирующий отсортированный поток. Элементы сортируются по возрастанию (естественный порядок).
Общая форма второй реализации метода следующая:
-
- T – тип элементов потока;
- Stream<T> – результирующий поток, отсортированный в порядке убывания;
- Comparator<? super T> – тип стандартного функционального интерфейса Java, который содержит метод, сравнивающий два значения;
- comparator – метод, в который вписан код лямбда-выражения. Лямбда-выражение получает два параметра и сравнивает их. Выражение возвращает целое число (<0, == 0,> 0) в зависимости от того, первый параметр меньше (равен, больше) чем второй параметр.
Метод sorted() является промежуточной операцией.
Пример. В примере демонстрируется применения метода sorted() для сортировки строк:
Interface Stream<T>
In addition to Stream , which is a stream of object references, there are primitive specializations for IntStream , LongStream , and DoubleStream , all of which are referred to as «streams» and conform to the characteristics and restrictions described here.
To perform a computation, stream operations are composed into a stream pipeline. A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which transform a stream into another stream, such as filter(Predicate) ), and a terminal operation (which produces a result or side-effect, such as count() or forEach(Consumer) ). Streams are lazy; computation on the source data is only performed when the terminal operation is initiated, and source elements are consumed only as needed.
A stream implementation is permitted significant latitude in optimizing the computation of the result. For example, a stream implementation is free to elide operations (or entire stages) from a stream pipeline — and therefore elide invocation of behavioral parameters — if it can prove that it would not affect the result of the computation. This means that side-effects of behavioral parameters may not always be executed and should not be relied upon, unless otherwise specified (such as by the terminal operations forEach and forEachOrdered ). (For a specific example of such an optimization, see the API note documented on the count() operation. For more detail, see the side-effects section of the stream package documentation.)
Collections and streams, while bearing some superficial similarities, have different goals. Collections are primarily concerned with the efficient management of, and access to, their elements. By contrast, streams do not provide a means to directly access or manipulate their elements, and are instead concerned with declaratively describing their source and the computational operations which will be performed in aggregate on that source. However, if the provided stream operations do not offer the desired functionality, the BaseStream.iterator() and BaseStream.spliterator() operations can be used to perform a controlled traversal.
A stream pipeline, like the «widgets» example above, can be viewed as a query on the stream source. Unless the source was explicitly designed for concurrent modification (such as a ConcurrentHashMap ), unpredictable or erroneous behavior may result from modifying the stream source while it is being queried.
- must be non-interfering (they do not modify the stream source); and
- in most cases must be stateless (their result should not depend on any state that might change during execution of the stream pipeline).
Such parameters are always instances of a functional interface such as Function , and are often lambda expressions or method references. Unless otherwise specified these parameters must be non-null.
A stream should be operated on (invoking an intermediate or terminal stream operation) only once. This rules out, for example, «forked» streams, where the same source feeds two or more pipelines, or multiple traversals of the same stream. A stream implementation may throw IllegalStateException if it detects that the stream is being reused. However, since some stream operations may return their receiver rather than a new stream object, it may not be possible to detect reuse in all cases.
Перевод руководства по Stream API от Benjamin Winterberg
Привет, Хабр! Представляю вашему вниманию перевод статьи «Java 8 Stream Tutorial».
Это руководство, основанное на примерах кода, представляет всесторонний обзор потоков в Java 8. При моем первом знакомстве с Stream API, я был озадачен названием, поскольку оно очень созвучно с InputStream и OutputStream из пакета java.io; Однако потоки в Java 8 — нечто абсолютно другое. Потоки представляют собой монады, которые играют важную роль в развитии функционального программирования в Java.
Это руководство научит работе с потоками и покажет как обращаться с различными методами, доступными в Stream API. Мы разберем порядок выполнения операций и проследим как последовательность методов в цепочке влияет на производительность. Познакомимся с мощными методами Stream API, такими как reduce , collect и flatMap . В конце руководства уделим внимание параллельной работе с потоками.
Если вы не чувствуете себя свободно в работе с лямбда-выражениями, функциональными интерфейсами и ссылочными методами, вам будет полезно ознакомиться с моим руководством по нововведениям в Java 8 (перевод на Хабре), а после этого вернуться к изучению потоков.
Как работают потоки
Поток представляет последовательность элементов и предоставляет различные методы для произведения вычислений над данными элементами:
Методы потоков бывают промежуточными (intermediate) и терминальными (terminal). Промежуточные методы возвращают поток, что позволяет последовательно вызывать множество таких методов. Терминальные методы либо не возвращают значения (void) либо возвращают результат типа отличного от потока. В вышеприведенном примере методы filter , map и sorted являются промежуточными, а forEach — терминальным. Для ознакомления с полным списком доступных методов потока обратитесь к документации. Такая цепочка потоковых операций также известна как конвейер операций (operation pipeline).
Большинство методов из Stream API принимают в качестве параметров лямбда-выражения, функциональный интерфейс, описывающие конкретное поведение метода. Большая их часть должна одновременно быть невмешивающейся (non-interfering) и не запоминающей состояние (stateless). Что же это означает?
Метод является невмешивающимся (non-interfering), если он не изменяет исходные данные, лежащие в основе потока. Например, в вышеприведенном примере никакие лямбда-выражения не вносят изменений в списочный массив myList.
Метод является не запоминающим состояние (stateless), если порядок выполнения операции определен. Например, ни одно лямбда-выражение из примера не зависит от изменяемых переменных или состояний внешнего пространства, которые могли бы меняться во время выполнения.
Различные виды потоков
Потоки могут быть созданы из различных исходных данных, главным образом из коллекций. Списки (Lists) и множества (Sets) поддерживают новые методы stream() и parllelStream() для создания последовательных и параллельных потоков. Параллельные потоки способны работать в многопоточном режиме (on multiple threads) и будут рассмотрены в конце руководства. А пока рассмотрим последовательные потоки:
Здесь вызов метода stream() для списка возвращает обычный объект потока.
Однако для работы с потоком вовсе не обязательно создавать коллекцию:Просто используйте Stream.of() для создания потока из нескольких объектных ссылок.
Помимо обычных потоков объектов Java 8 располагает специальными типами потоков для работы с примитивными типами: int, long, double. Как можно догадаться это IntStream , LongStream , DoubleStream .
Потоки IntStream могут заменить обычные циклы for(;;) используя IntStream.range() :
Все эти потоки для работы с примитивными типами работают так же как и обычные потоки объектов за исключением следующего:
- Потоки примитивов используют специальные лямбда-выражения. Например, IntFunction вместо Function, или IntPredicate вместо Predicate.
- Потоки примитивов поддерживают дополнительные терминальные методы: sum() и average()
Потоки примитивов могут быть преобразованы в потоки объектов посредством вызова mapToObj() :
В следующем примере поток из чисел с плавающей точкой отображается в поток целочисленных чисел и затем отображается в поток объектов:
Порядок выполнения
Сейчас, когда мы узнали как создавать различные потоки и как с ними работать, погрузимся глубже и рассмотрим, как потоковые операции выглядят под капотом.
Важная характеристика промежуточных методов — их лень. В этом примере отсутствует терминальный метод:
При выполнении этого фрагмента кода ничего не будет выведено в консоль. А все потому, что промежуточные методы выполняются только при наличии терминального метода. Давайте расширим пример добавлением терминального метода forEach :
Выполнение этого фрагмента кода приводит к выводу на консоль следующего результата:
Порядок, в котором расположены результаты, может удивить. Можно наивно ожидать, что методы будут выполняться “горизонтально”: один за другим для всех элементов потока. Однако вместо этого элемент двигается по цепочке “вертикально”. Сначала первая строка “d2” проходит через метод filter затем через forEach и только тогда, после прохода первого элемента через всю цепочку методов, следующий элемент начинает обрабатываться.
Принимая во внимание такое поведение, можно уменьшить фактическое количество операций:
Метод anyMatch вернет true, как только предикат будет применен к входящему элементу. В данном случае это второй элемент последовательности — “A2”. Соответственно, благодаря “вертикальному” выполнению цепочки потока map будет вызван только дважды. Таким образом вместо отображения всех элементов потока, map будет вызван минимально возможное количество раз.
Почему последовательность имеет значение
Следующий пример состоит из двух промежуточных методов map и filter и терминального метода forEach . Рассмотрим как выполняются данные методы:
Нетрудно догадаться, что оба метода map и filter вызываются 5 раз за время выполнения — по разу для каждого элемента исходной коллекции, в то время как forEach вызывается только единожды — для элемента прошедшего фильтр.
Можно существенно сократить число операций, если изменить порядок вызовов методов, поместив filter на первое место:
Сейчас map вызывается только один раз. При большом количестве входящих элементов будем наблюдать ощутимый прирост производительности. Помните об этом составляя сложные цепочки методов.
Расширим вышеприведенный пример, добавив дополнительную операцию сортировки — метод sorted :
Сортировка — это специальный вид промежуточных операций. Это так называемая операция с запоминанием состояния (stateful), поскольку для сортировки коллекции необходимо учитывать ее состояния на протяжении всей операции.
В результате выполнения данного кода получаем следующий вывод в консоль:
Сперва производится сортировка всей коллекции целиком. Другими словами метод sorted выполняется “горизонтально”. В данном случае sorted вызывается 8 раз для нескольких комбинаций из элементов входящей коллекции.
Еще раз оптимизируем выполнение данного кода посредством изменения порядка вызовов методов в цепочке:
В этом примере sorted вообще не вызывается т.к. filter сокращает входную коллекцию до одного элемента. В случае с большими входящими данными производительность выиграет существенно.
Повторное использование потоков
В Java 8 потоки не могут быть использованы повторно. После вызова любого терминального метода поток завершается:
Вызов noneMatch после anyMatch в одном потоке приводит к следующей исключительной ситуации:
Для преодоления этого ограничения следует создавать новый поток для каждого терминального метода.
Например, можно создать поставщика (supplier) для конструктора нового потока, в котором будут установлены все промежуточные методы:
Каждый вызов метода get создает новый поток, в котором можно безопасно вызвать желаемый терминальный метод.
Продвинутые методы
Потоки поддерживают большое количество различных методов. Мы уже ознакомились с наиболее важными методами. Чтобы самостоятельно ознакомиться с остальными, обратитесь к документации. А сейчас погрузимся еще глубже в более сложные методы: collect , flatMap и reduce .
Большая часть примеров кода из этого раздела обращается к следующему фрагменту кода для демонстрации работы:
Collect
Collect очень полезный терминальный метод, который служит для преобразования элементов потока в результат иного типа, например, List, Set или Map.
Collect принимает Collector , который содержит четыре различных метода: поставщик (supplier). аккумулятор (accumulator), объединитель (combiner), финишер (finisher). На первый взгляд это выглядит очень сложно, однако Java 8 поддерживает различные встроенные коллекторы через класс Collectors , где реализованы наиболее используемые методы.
Как видите, создать список из элементов потока очень просто. Нужен не список а множество? Используйте Collectors.toSet() .
В следующем примере люди группируются по возрасту:
Коллекторы невероятно разнообразны. Также можно агрегировать элементы коллекции, например, определить средний возраст:
Для получения более исчерпывающей статистики используем резюмирующий коллектор, который возвращает специальный объект с информацией: минимальным, максимальным и средним значениями, суммой значений и количеством элементов:
Следующий пример объединяет все имена в одну строку:
Соединяющий коллектор принимает разделитель, а также опционально префикс и суффикс.
Для преобразования элементов потока в отображение, следует определить каким образом должны отображаться ключи и значения. Помните, что ключи в отображении должны быть уникальными. В противном случае получим IllegalStateException . Можно опционально добавить функцию слияния для обхода исключения:
Итак, мы ознакомились с некоторыми из наиболее мощных встроенных коллекторов. Попробуем построить собственный. Мы хотим преобразовать все элементы потока в одну строку, которая состоит из имен в верхнем регистре, разделенных вертикальной чертой |. Для этого создадим новый коллектор используя Collector.of() . Нам нужны четыре составные части нашего коллектора: поставщик, аккумулятор, соединитель, финишер.
Поскольку строки в Java неизменяемы, нам нужен класс-помощник типа StringJoiner , позволяющий коллектору построить для нас строку. На первой стадии поставщик конструирует StringJoiner с присвоенным разделителем. Аккумулятор используется для добавления каждого имени в StringJoiner .
Соединитель знает как соединить два StringJoiner а в один. И в конце финишер конструирует желаемую строку из StringJoiner ов.
FlatMap
Итак, мы узнали как превращать объекты потока в другие типы объектов при помощи метода map . Map — своего рода ограниченный метод, поскольку каждый объект может быть отображен в только один другой объект. Но что если нужно отобразить один объект в множество других, или вовсе не отображать его? Вот тут-то выручает метод flatMap . FlatMap превращает каждый объект потока в поток других объектов. Содержимое этих потоков затем упаковывается в возвращаемый поток метода flatMap .
Для того чтобы посмотреть на flatMap в действии, соорудим подходящую иерархию типов для примера:
Создадим несколько объектов:
Теперь у нас есть список из трех foo, каждый из которых содержит по три bar.
FlatMap принимает функцию, которая должна вернуть поток объектов. Таким образом, чтобы получить доступ к объектам bar каждого foo, нам просто нужно подобрать подходящую функцию:
Итак, мы успешно превратили поток из трех объектов foo в поток из 9 объектов bar.
Наконец, весь вышеприведенный код можно сократить до простого конвейера операций:
FlatMap также доступен в классе Optional , введенном в Java 8. FlatMap из класса Optional возвращает опциональный объект другого класса. Это может быть использовано чтобы избежать нагромождения проверок на null .
Представьте себе иерархическую структуру типа этой:
Для получения вложенной строки foo из внешнего объекта необходимо добавить множественные проверки на null для избежания NullPointException :
Того же можно добиться, используя flatMap класса Optional:
Каждый вызов flatMap возвращает обертку Optional для желаемого объекта, если он присутствует, либо для null в случае отсутствия объекта.
Reduce
Операция упрощения объединяет все элементы потока в один результат. Java 8 поддерживает три различных типа метода reduce.
Первый сокращает поток элементов до единственного элемента потока. Используем этот метод для определения элемента с наибольшим возрастом:
Метод reduce принимает аккумулирующую функцию с бинарным оператором (BinaryOperator). Тут reduce является би-функцией (BiFunction), где оба аргумента принадлежат одному типу. В нашем случае, к типу Person. Би-функция — практически тоже самое, что и функция (Function), однако принимает 2 аргумента. В нашем примере функция сравнивает возраст двух людей и возвращает элемент с большим возрастом.
Следующий вид метода reduce принимает и начальное значение, и аккумулятор с бинарным оператором. Этот метод может быть использован для создания нового элемента. У нас — Person с именем и возрастом, состоящими из сложения всех имен и суммы прожитых лет:
Третий метод reduce принимает три параметра: изначальное значение, аккумулятор с би-функцией и объединяющую функцию типа бинарного оператора. Поскольку начальное значение типа не ограничено до типа Person, можно использовать редуцирование для определения суммы прожитых лет каждого человека:
Как видим, мы получили результат 76, но что же на самом деле происходит под капотом?
Расширим вышеприведенный фрагмент кода выводом текста для дебага:
Как видим, всю работу выполняет аккумулирующая функция. Впервые она вызывается с изначальным значением 0 и первым человеком Max. В последующих трех шагах sum постоянно возрастает на возраст человека из последнего шага пока не достигает общего возраста 76.
И что дальше? Объединитель никогда не вызывается? Рассмотрим параллельное выполнение этого потока:
При параллельном выполнении получаем совершенно другой консольный вывод. Сейчас объединитель действительно вызывается. Поскольку аккумулятор вызывался параллельно, объединитель должен был суммировать значения, сохраненные по-отдельности.
В следующей главе более детально изучим параллельное выполнение потоков.
Параллельные потоки
Потоки могут выполняться параллельно для повышения производительности при работе с большими количествами входящих элементов. Параллельные потоки используют обычный ForkJoinPool доступный через вызов статического метода ForkJoinPool.commonPool() . Размер основного пула потоков может достигать 5 потоков выполнения — точное число зависит от количества доступных физических ядер процессора.
На моем компьютере обычный пул потоков по умолчанию инициализируется с распараллеливанием на 3 потока. Это значение можно увеличить или уменьшить посредством установки следующего параметра JVM:
Коллекции поддерживают метод parallelStream() для создания параллельных потоков данных. Также можно вызвать промежуточный метод parallel() для превращения последовательного потока в параллельный.
Для понимания поведения потока при параллельном выполнении, следующий пример печатает информацию про каждый текущий поток (thread) в System.out :
Рассмотрим выводы с записями для дебага чтобы лучше понять, какой поток (thread) используется для выполнения конкретных методов потока (stream):
Как видим, при параллельном выполнении потока данных используются все доступные потоки (threads) текущего ForkJoinPool . Последовательность вывода может отличаться, так как не определена последовательность выполнения каждого конкретного потока (thread).
Давайте расширим пример добавлением метода sort :
На первый взгляд результат может показаться странным:
Кажется, будто sort выполняется последовательно и только в потоке main. На самом деле при параллельном выполнении потока под капотом метода sort из Stream API спрятан метод сортировки класса Arrays , добавленный в Java 8, — Arrays.parallelSort() . Как указано в документации, этот метод на основании длины входящей коллекции определяет, как именно — параллельно или последовательно будет произведена сортировка:
Вернемся к примеру с методом reduce из предыдущей главы. Мы уже выяснили, что объединительная функция вызывается только при параллельной работе с потоком. Рассмотрим, какие потоки задействованы:
Консольный вывод показывает, что обе функции: аккумулирующая и объединяющая, выполняются параллельно, используя все возможные потоки:
Можно утверждать, что параллельное выполнение потока способствует значительному повышению эффективности при работе с большими количествами входящих элементов. Однако следует помнить, что некоторые методы при параллельном выполнении требуют дополнительных расчетов (объединительных операций), которые не требуются при последовательном выполнении.
Кроме того, для параллельного выполнения потока используется все тот же ForkJoinPool , так широко используемый в JVM. Так что применение медленных блокирующих методов потока может негативно отразиться на производительности всей программы, за счет блокирования потоков (threads), используемых для обработки в других задачах.
Вот и все
Мое руководство по использованию потоков в Java 8 окончено. Для более подробного изучения работы с потоками можно обратиться к документации. Если вы хотите углубиться и больше узнать про механизмы, лежащие в основе работы потоков, вам может быть интересно прочитать статью Мартина Фаулера (Martin Fowler) Collection Pipelines.
Если вам так же интересен JavaScript, вы можете захотеть взглянуть на Stream.js — JavaScript реализацию Java 8 Streams API. Возможно, вы также захотите прочитать мои статьи Java 8 Tutorial (русский перевод на Хабре) и Java 8 Nashorn Tutorial.
Надеюсь, это руководство было полезным и интересным для вас, и вы наслаждались в процессе чтения. Полный код хранится в GitHub. Чувствуйте себя свободно, создавая ответвление в репозитории.
Кофе-брейк #177. Подробное руководство по Java Stream в Java 8

Источник: Hackernoon В этой публикации изложено подробное руководство по работе с Java Stream вместе с примерами кода и пояснениями.
Введение в потоки Java в Java 8
Java Stream можно использовать с помощью пакета “java.util.stream”. Его можно импортировать в скрипт с помощью кода:
Используя этот код, мы также можем легко реализовать несколько встроенных функций в Java Stream.
Java Stream может принимать входные данные из коллекций данных, таких как коллекции и массивы в Java.
Java Stream не требует изменения структуры входных данных.
Java Stream не изменяет источник. Вместо этого он генерирует выходные данные с помощью соответствующих конвейерных методов.
Java Stream подвергаются промежуточным и терминальным операциям, которые мы обсудим в следующих разделах.
В Java Stream промежуточные операции конвейеризированы и происходят в формате отложенных или “ленивых” вычислений (lazy evaluation). Они завершаются терминальными функциями. Это формирует базовый формат использования Java Stream.