Что такое stream java

Что нужно знать о Java Stream API

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Java-разработчик

Всем привет! В этой статье я хочу познакомить вас, на мой взгляд, с одним из самых значительных нововведений в Java со времен ее появления — это Java Stream API.

Что такое Java Stream API? Зачем? И какие дает преимущества?

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

Java Stream API был создан для того, чтобы помочь пользователям ускорить и упростить обработку данных. Сам по себе API предоставляет инструмент, который позволяет нам дать рецепт того как обрабатывать объекты.

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

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

Последний элемент в этой цепи — покупатель, который приходит на завод и делает заказ.

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

В мире Java такой завод называется Stream API. Этот API представляет собой библиотеку, которая помогает в функциональном стиле кратко, но емко описывать, как обработать данные.

Как и в примере про завод, у каждого стрима должен быть источник объектов. Этим источником информации чаще всего бывает коллекция, так как именно в них мы и храним наши данные, но это не обязательно — может быть и какой-то генератор, который генерирует объекты по заданному правилу, примеры мы рассмотрим позже.

В Java Stream API также предусмотрены промежуточные операции. Они выполняют роль рабочих. Операции описывают процесс обработки объектов.

В конце каждого стрима должна быть терминальная операция, которая должна поглотить все обработанные данные.

В примере про завод мы видели, что заказчик становится триггером начала производства и является последним звеном в работе завода — он забирает всю продукцию.

Рассмотрим простейший стрим. Создадим класс бревно и поместим несколько бревен в коллекцию:

Получив ссылку на стрим, мы можем начать обрабатывать поток наших данных.

Отфильтруем бревна, количество которых меньше 7 и оставим только те, которые не являются дубом. Выглядеть это будет так:

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

В этом примере конечная операция принимает оставшиеся элементы после фильтрации и распечатывает их. Стоит особо упомянуть, что второй раз вызвать терминальную операцию не получится — стрим является «одноразовым» объектом. Это сделано авторами библиотеки для того, чтобы можно было корректно обрабатывать данные, которые имеют ограниченное время жизни. Например, если обрабатывать пакеты из интернета, то данные в стрим могут попасть только один раз, поэтому повторный вызов теряет всякий смысл.

Как упоминалось ранее, создать источник данных можно разными способами. Рассмотрим самые популярные.

Способы создания источника данных

В начале пройдемся по методам объявленным в интерфейсе Stream.

Stream.of(). Метод принимает массив объектов и создает на их основе стрим.

Для создания пустого стрима существует метод:

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

Если у нас есть два стрима, мы можем объеденить их в один вызвав метод:

В итоге мы получим стрим, в котором будет находится шесть элементов.

Стрим не обязательно должен поглощать какие-то данные, можно создать генератор, который будет поставлять в наш стрим с помощью метода generate()

Так как генератор может бесконечно генерировать стрим и в примере выше мы получим бесконечный вывод на экран случайных значений, необходимо добавить промежуточную операцию limit(100) — она позволит ограничить стрим. С этими операциями мы познакомимся позже.

Аналогичную функциональность предоставляет класс Random. В нем уже есть методы которые создают стримы из случайных чисел.

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

Поэтому создатели стримов добавили специальные типы стримов для примитивных типов:

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

Теперь мы перейдем к самому интересному — в интерфейсе Collection добавлен дефолтный метод, который возвращает нам стрим. То есть любая коллекция дает нам возможность превратить ее в стрим:

Просто вызвав метод у коллекции мы получили стрим. Это самый частый способ получить стрим из набора данных.

Познакомившись с основными методами создания теперь мы можем перейти к промежуточным операциям. Именно они позволят нам обработать наш поток данных.

Промежуточные операции

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

Но на нашем заводе мы делаем намного больше чем просто фильтруем бревна. Для того, чтобы дерево превратилось в мебель его нужно преобразовать.

Для этого пригодится самая популярная функция — map().

Возьмем наш пример выше и попробуем преобразовать

Промежуточные операции можно конкатенировать между собой, то есть мы можем добавить еще несколько преобразований:

Во втором преобразовании мы разбили каждую строку на массив строк. Но если мы запустим приложение, мы увидим, что на экран не вывелись строки, а вывелось toString() массивов. Нам хочется чтобы стрим был плоский — то есть только из объектов, а не из других стримов/массивов в которых есть объекты. Для этого авторы Java Stream API придумали еще одну промежуточную операцию — flatMap. Вот как она позволит изменить нам наш стрим (для более краткой записи я заменил прошлые операции на метод референс):

Но запустив пример выше мы получили стрим стримов — Stream

Но так с ним работать не удобно, а обычный flatMap не сработает, поэтому для примитивных стримов существуют специальные операции для их преобразований:

Для того чтобы отсортировать буквы воспользуемся операцией sorted() :

Стоит отметить, что операция sorted() таит в себе некоторые проблемы. Так для того чтобы отсортировать объекты, поступающие из стрима, она должна аккумулировать в себе все объекты, которые есть в стриме и только потом приступить к сортировке. Но что делать, если стрим бесконечный либо в стриме огромное количество элементов? Вызов такой операции приведет к OutOfMemoryException.

Простой пример приведен ниже:

Стрим будет генерировать новые значения, пока остаток от деления на 7 сгенерированного значения не будет равен 0.

Терминальные операции

После знакомства с основными промежуточными операциями мы плавно подошли к заключению. Осталось рассмотреть терминальные операции. Это операции, которые как бы «запускают» наш стрим. Мы можем создать стрим и добавить в него любое количество промежуточных операций, но они не будут выполнены пока не будут добавлена терминальная операция.

Кроме этого терминальная операция может и возвращать значение. Рассмотрим самые распространенные — findFirst(), findAny(), anyMatch(), allMatch(), noneMatch().

Теперь стоит перейти к более сложным функциям. Часто в качестве результата стрима мы хотим получить набор из новых объектов, которые были созданы в результате обработки. Для этого удобно поместить их в массив или коллекцию.

В Java Stream API было добавлено несколько методов, которые дают соответствующую функциональность.

Вызвав терминальную операцию Object[] toArray() мы получим ссылку на массив, в котором будет находится все объекты. Если нужно вернуть массив определенного типа, то в метод стоить передать IntFunction generator на вход функции поступит число элементов, а внутри нее мы должны создать нужный нам тип массива.

Следующая операция, которую стоить упомянуть T reduce(T identity, BinaryOperator accumulator) — в нее передается начальное значение и бинарная функция, которая задает алгоритм объединения двух объектов.

Для того, чтобы получить сумму первых 100 членов стрима из произвольных значений, запишем:

Мы передаем начальный элемент для сложения, в нашем случае он 0 и бинарную функцию, которая описывает как объединить два значения из стрима.

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

В функции reduce мы передали наш начальный аргумент — новую пустую коллекцию. Потом описали правило, по которому будем объединять коллекцию и элементы стрима.

И в конце описали как мы будем объединять две коллекции.

То есть вся логика комбинирования элементов хранится в структуре данных под названием коллектор.

Создатели Java Stream API добавили в библиотеку большое количество коллекторов, рассмотрим их.

Существует более общий метод Collectors.toCollection(). В качестве аргумента в нее можно передать коллекцию, в которую будут помещены элементы стрима.

Операция partitionBy() позволяет разделить стрим на два множества по определенному условию. Например мы хотим разделить наш буквенный стрим на две группы с большими буквами и прописными:

Коллекторы могут быть скомбинированы друг с другом, что дает большую гибкость.

В примере выше мы видим, что некоторые буквы повторяются, мы этого не хотим поэтому добавим еще один коллектор, который соберет все в Set:

Чтобы самостоятельно реализовать коллектор можно воспользоваться статическим методом:

В этой короткой статье мы познакомились с, на мой взгляд, самой крутой штукой в языке Java с момента ее создания. Стримы позволяют существенно упростить, а соответственно ускорить разработку кода. Возможность практически бесплатно сделать стрим параллельным, тем самым повысив производительность кода в разы, делает стримы инструментом номер одни в руках каждого разработчика.

Источник

Java Stream API. Копилка рецептов

Если вы не любите стримы, возможно, вы пока не умеете их готовить 🙂 Приглашаем поучиться.

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

pavel starikov / flickr

В этой статье почти нет теории, зато много практики и кода. Разберём семь типичных ситуаций, когда стримы бывают полезны. Сравним решения с классическими императивными реализациями.

Stream API — что это вообще такое

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

О началах функционального программирования и лямбдах в Java читайте здесь.

Стрим — это объект для универсальной работы с данными. И это вовсе не какая-то новая структура данных, он использует существующие коллекции для получения новых элементов.

Затем к данным применяются методы. В интерфейсе Stream их множество. Каждый выполняет одну из типичных операций с коллекцией: отсортировать, перегруппировать, отфильтровать. Мы разберём некоторые из этих методов дальше.

Думайте о стриме как о потоке данных, а о цепочке вызовов методов — как о конвейере.

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

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

Преимущества

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

Есть и другие плюсы:

А теперь, когда вы почти поверили, что стримы — это хорошо, перейдём к практике.

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.

Подготовим данные

Работу методов Java Stream API покажем на примере офлайновой библиотеки. Для каждой книги библиотечного фонда известны автор, название и год издания.

Для читателя библиотеки будем хранить ФИО и электронный адрес. Каждый читатель может взять в библиотеке одну или несколько книг — их тоже сохраним.

Ещё нам понадобится флаг читательского согласия на уведомления по электронной почте. Рассылки организуют сотрудники библиотеки: напоминают о сроке возврата книг, сообщают новости.

Источник

Stream API

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Что такое Stream API?

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

Поэтому каждый раз новый:

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

Далее давайте рассмотрим некоторые промежуточные операторы:

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

IntStream.range(0,x) – выдаёт на поток элементов с 0 (включительно) по x (не включительно);

limit(long maxSize) – ограничивает стрим по количеству элементов:

skip(long n) – пропускаем n элементов:

distinct() — проверяет стрим на уникальность элементов(убирает повторы элементов);

dropWhile(Predicate predicate) — пропускает элементы которые удовлетворяют условию (появился в 9 java, Функциональный интерфейс Predicate проверяет соблюдение некоторого условия. Если оно соблюдается, то возвращается значение true. В качестве параметра лямбда-выражение принимает объект типа T:

Что такое stream java. Смотреть фото Что такое stream java. Смотреть картинку Что такое stream java. Картинка про Что такое stream java. Фото Что такое stream java

forEach(Consumer action) – аналог for each (Consumer выполняет некоторое действие над объектом типа T, при этом ничего не возвращая);

count() – возвращает количество елементов стрима:

collect(Collector collector) – метод собирает все элементы в список, множество или другую коллекцию, сгруппировывает элементы по какому-нибудь критерию, объединяет всё в строку и т.д.:

reduce(T identity, BinaryOperator accumulator) — преобразовывает все элементы стрима в один объект(посчитать сумму всех элементов, либо найти минимальный элемент), cперва берётся объект identity и первый элемент стрима, применяется функция accumulator и identity становится её результатом. Затем всё продолжается для остальных элементов.

Optional min(Comparator comparator)
Optional max(Comparator comparator) ищет минимальный/максимальный элемент, основываясь на переданном компараторе;

findFirst() – вытаскивает первый элемент стрима:

allMatch(Predicate predicate) — возвращает true, если все элементы стрима удовлетворяют условию. Если встречается какой-либо элемент, для которого результат вызова функции-предиката будет false, то оператор перестаёт просматривать элементы и возвращает false:

anyMatch(Predicate predicate) — вернет true, если хотя бы один элемент стрима удовлетворяет условию predicate :

noneMatch(Predicate predicate) — вернёт true, если, пройдя все элементы стрима, ни один не удовлетворил условию predicate :

toList() — собирает элементы в List :

toSet() — cобирает элементы в множество:

counting() — Подсчитывает количество элементов:

joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix) — cобирает элементы в одну строку. Дополнительно можно указать разделитель, а также префикс и суффикс для всей последовательности:

summingDouble(ToDoubleFunction mapper) — коллектор, который преобразовывает объекты в int/long/double и подсчитывает сумму.

Источник

Перевод руководства по Stream API от Benjamin Winterberg

Привет, Хабр! Представляю вашему вниманию перевод статьи «Java 8 Stream Tutorial».

Это руководство, основанное на примерах кода, представляет всесторонний обзор потоков в Java 8. При моем первом знакомстве с Stream API, я был озадачен названием, поскольку оно очень созвучно с InputStream и OutputStream из пакета java.io; Однако потоки в Java 8 — нечто абсолютно другое. Потоки представляют собой монады, которые играют важную роль в развитии функционального программирования в Java.

В функциональном программировании монада является структурой, которая представляет вычисление в виде цепи последовательных шагов. Тип и структура монады определяют цепочку операций, в нашем случае — последовательность методов с встроенными функциями заданного типа.

Если вы не чувствуете себя свободно в работе с лямбда-выражениями, функциональными интерфейсами и ссылочными методами, вам будет полезно ознакомиться с моим руководством по нововведениям в Java 8 (перевод на Хабре), а после этого вернуться к изучению потоков.

Как работают потоки

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

Большинство методов из Stream API принимают в качестве параметров лямбда-выражения, функциональный интерфейс, описывающие конкретное поведение метода. Большая их часть должна одновременно быть невмешивающейся (non-interfering) и не запоминающей состояние (stateless). Что же это означает?

Метод является невмешивающимся (non-interfering), если он не изменяет исходные данные, лежащие в основе потока. Например, в вышеприведенном примере никакие лямбда-выражения не вносят изменений в списочный массив myList.

Метод является не запоминающим состояние (stateless), если порядок выполнения операции определен. Например, ни одно лямбда-выражение из примера не зависит от изменяемых переменных или состояний внешнего пространства, которые могли бы меняться во время выполнения.

Различные виды потоков

Потоки могут быть созданы из различных исходных данных, главным образом из коллекций. Списки (Lists) и множества (Sets) поддерживают новые методы stream() и parllelStream() для создания последовательных и параллельных потоков. Параллельные потоки способны работать в многопоточном режиме (on multiple threads) и будут рассмотрены в конце руководства. А пока рассмотрим последовательные потоки:

Здесь вызов метода stream() для списка возвращает обычный объект потока.
Однако для работы с потоком вовсе не обязательно создавать коллекцию:

Просто используйте Stream.of() для создания потока из нескольких объектных ссылок.

Потоки IntStream могут заменить обычные циклы for(;;) используя IntStream.range() :

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

Потоки примитивов могут быть преобразованы в потоки объектов посредством вызова mapToObj() :

В следующем примере поток из чисел с плавающей точкой отображается в поток целочисленных чисел и затем отображается в поток объектов:

Порядок выполнения

Сейчас, когда мы узнали как создавать различные потоки и как с ними работать, погрузимся глубже и рассмотрим, как потоковые операции выглядят под капотом.

Важная характеристика промежуточных методов — их лень. В этом примере отсутствует терминальный метод:

При выполнении этого фрагмента кода ничего не будет выведено в консоль. А все потому, что промежуточные методы выполняются только при наличии терминального метода. Давайте расширим пример добавлением терминального метода forEach :

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

Порядок, в котором расположены результаты, может удивить. Можно наивно ожидать, что методы будут выполняться “горизонтально”: один за другим для всех элементов потока. Однако вместо этого элемент двигается по цепочке “вертикально”. Сначала первая строка “d2” проходит через метод filter затем через forEach и только тогда, после прохода первого элемента через всю цепочку методов, следующий элемент начинает обрабатываться.

Принимая во внимание такое поведение, можно уменьшить фактическое количество операций:

Метод anyMatch вернет true, как только предикат будет применен к входящему элементу. В данном случае это второй элемент последовательности — “A2”. Соответственно, благодаря “вертикальному” выполнению цепочки потока map будет вызван только дважды. Таким образом вместо отображения всех элементов потока, map будет вызван минимально возможное количество раз.

Почему последовательность имеет значение

Нетрудно догадаться, что оба метода map и filter вызываются 5 раз за время выполнения — по разу для каждого элемента исходной коллекции, в то время как forEach вызывается только единожды — для элемента прошедшего фильтр.

Можно существенно сократить число операций, если изменить порядок вызовов методов, поместив filter на первое место:

Сейчас map вызывается только один раз. При большом количестве входящих элементов будем наблюдать ощутимый прирост производительности. Помните об этом составляя сложные цепочки методов.

Расширим вышеприведенный пример, добавив дополнительную операцию сортировки — метод sorted :

Сортировка — это специальный вид промежуточных операций. Это так называемая операция с запоминанием состояния (stateful), поскольку для сортировки коллекции необходимо учитывать ее состояния на протяжении всей операции.

В результате выполнения данного кода получаем следующий вывод в консоль:

Сперва производится сортировка всей коллекции целиком. Другими словами метод sorted выполняется “горизонтально”. В данном случае sorted вызывается 8 раз для нескольких комбинаций из элементов входящей коллекции.

Еще раз оптимизируем выполнение данного кода посредством изменения порядка вызовов методов в цепочке:

В этом примере sorted вообще не вызывается т.к. filter сокращает входную коллекцию до одного элемента. В случае с большими входящими данными производительность выиграет существенно.

Повторное использование потоков

В Java 8 потоки не могут быть использованы повторно. После вызова любого терминального метода поток завершается:

Вызов noneMatch после anyMatch в одном потоке приводит к следующей исключительной ситуации:

Для преодоления этого ограничения следует создавать новый поток для каждого терминального метода.

Например, можно создать поставщика (supplier) для конструктора нового потока, в котором будут установлены все промежуточные методы:

Каждый вызов метода get создает новый поток, в котором можно безопасно вызвать желаемый терминальный метод.

Продвинутые методы

Большая часть примеров кода из этого раздела обращается к следующему фрагменту кода для демонстрации работы:

Collect

Collect очень полезный терминальный метод, который служит для преобразования элементов потока в результат иного типа, например, List, Set или Map.

В следующем примере люди группируются по возрасту:

Коллекторы невероятно разнообразны. Также можно агрегировать элементы коллекции, например, определить средний возраст:

Для получения более исчерпывающей статистики используем резюмирующий коллектор, который возвращает специальный объект с информацией: минимальным, максимальным и средним значениями, суммой значений и количеством элементов:

Следующий пример объединяет все имена в одну строку:

Соединяющий коллектор принимает разделитель, а также опционально префикс и суффикс.

Соединитель знает как соединить два StringJoiner а в один. И в конце финишер конструирует желаемую строку из StringJoiner ов.

FlatMap

Для того чтобы посмотреть на flatMap в действии, соорудим подходящую иерархию типов для примера:

Создадим несколько объектов:

Теперь у нас есть список из трех foo, каждый из которых содержит по три bar.

FlatMap принимает функцию, которая должна вернуть поток объектов. Таким образом, чтобы получить доступ к объектам bar каждого foo, нам просто нужно подобрать подходящую функцию:

Итак, мы успешно превратили поток из трех объектов foo в поток из 9 объектов bar.

Наконец, весь вышеприведенный код можно сократить до простого конвейера операций:

Представьте себе иерархическую структуру типа этой:

Для получения вложенной строки 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.

И что дальше? Объединитель никогда не вызывается? Рассмотрим параллельное выполнение этого потока:

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

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

Параллельные потоки

На моем компьютере обычный пул потоков по умолчанию инициализируется с распараллеливанием на 3 потока. Это значение можно увеличить или уменьшить посредством установки следующего параметра JVM:

Коллекции поддерживают метод parallelStream() для создания параллельных потоков данных. Также можно вызвать промежуточный метод parallel() для превращения последовательного потока в параллельный.

Для понимания поведения потока при параллельном выполнении, следующий пример печатает информацию про каждый текущий поток (thread) в System.out :

Рассмотрим выводы с записями для дебага чтобы лучше понять, какой поток (thread) используется для выполнения конкретных методов потока (stream):

Давайте расширим пример добавлением метода sort :

На первый взгляд результат может показаться странным:

Если длина определенного массива меньше минимальной “зернистости”, сортировка производится посредством выполнения метода Arrays.sort.

Вернемся к примеру с методом reduce из предыдущей главы. Мы уже выяснили, что объединительная функция вызывается только при параллельной работе с потоком. Рассмотрим, какие потоки задействованы:

Консольный вывод показывает, что обе функции: аккумулирующая и объединяющая, выполняются параллельно, используя все возможные потоки:

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

Вот и все

Мое руководство по использованию потоков в Java 8 окончено. Для более подробного изучения работы с потоками можно обратиться к документации. Если вы хотите углубиться и больше узнать про механизмы, лежащие в основе работы потоков, вам может быть интересно прочитать статью Мартина Фаулера (Martin Fowler) Collection Pipelines.

Если вам так же интересен JavaScript, вы можете захотеть взглянуть на Stream.js — JavaScript реализацию Java 8 Streams API. Возможно, вы также захотите прочитать мои статьи Java 8 Tutorial (русский перевод на Хабре) и Java 8 Nashorn Tutorial.

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *