какие типы операций есть в стримах

Stream API

Введение в Stream API

Одной из отличительных черт Stream API является применение лямбда-выражений, которые позволяют значительно сократить запись выполняемых действий.

При ближайшем рассмотрении мы можем найти в других технологиях программирования аналоги подобного API. В частности, в языке C# некоторым аналогом Stream API будет технология LINQ.

Рассмотрим простейший пример. Допустим, у нас есть задача: найти в массиве количество всех чисел, которые больше 0. До JDK 8 мы бы могли написать что-то наподобие следующего:

Теперь применим Stream API:

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

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

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

BaseStream определяет базовый функционал для работы с потоками, которые реализуется через его методы:

void close() : закрывает поток

boolean isParallel() : возвращает true, если поток является параллельным

Iterator iterator() : возвращает ссылку на итератор потока

Spliterator spliterator() : возвращает ссылку на сплитератор потока

S parallel() : возвращает параллельный поток (параллельные потоки могут задействовать несколько ядер процессора в многоядерных архитектурах)

S sequential() : возвращает последовательный поток

S unordered() : возвращает неупорядоченный поток

От интерфейса BaseStream наследуется ряд интерфейсов, предназначенных для создания конкретных потоков:

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

IntStream : используется для потоков с типом данных int

DoubleStream : используется для потоков с типом данных double

LongStream : используется для потоков с типом данных long

boolean allMatch(Predicate predicate) : возвращает true, если все элементы потока удовлетворяют условию в предикате. Терминальная операция

boolean anyMatch(Predicate predicate) : возвращает true, если хоть один элемент потока удовлетворяют условию в предикате. Терминальная операция

long count() : возвращает количество элементов в потоке. Терминальная операция.

Stream concat​(Stream a, Stream b) : объединяет два потока. Промежуточная операция

Stream distinct() : возвращает поток, в котором имеются только уникальные данные с типом T. Промежуточная операция

Stream dropWhile​(Predicate predicate) : пропускает элементы, которые соответствуют условию в predicate, пока не попадется элемент, который не соответствует условию. Выбранные элементы возвращаются в виде потока. Промежуточная операция.

Stream filter(Predicate predicate) : фильтрует элементы в соответствии с условием в предикате. Промежуточная операция

Optional findFirst() : возвращает первый элемент из потока. Терминальная операция

Optional findAny() : возвращает первый попавшийся элемент из потока. Терминальная операция

void forEach(Consumer action) : для каждого элемента выполняется действие action. Терминальная операция

Stream limit(long maxSize) : оставляет в потоке только maxSize элементов. Промежуточная операция

Optional max(Comparator comparator) : возвращает максимальный элемент из потока. Для сравнения элементов применяется компаратор comparator. Терминальная операция

Optional min(Comparator comparator) : возвращает минимальный элемент из потока. Для сравнения элементов применяется компаратор comparator. Терминальная операция

Stream map(Function mapper) : преобразует элементы типа T в элементы типа R и возвращает поток с элементами R. Промежуточная операция

Stream flatMap(Function > mapper) : позволяет преобразовать элемент типа T в несколько элементов типа R и возвращает поток с элементами R. Промежуточная операция

boolean noneMatch(Predicate predicate) : возвращает true, если ни один из элементов в потоке не удовлетворяет условию в предикате. Терминальная операция

Stream skip(long n) : возвращает поток, в котором отсутствуют первые n элементов. Промежуточная операция.

Stream sorted() : возвращает отсортированный поток. Промежуточная операция.

Stream sorted(Comparator comparator) : возвращает отсортированный в соответствии с компаратором поток. Промежуточная операция.

Stream takeWhile​(Predicate predicate) : выбирает из потока элементы, пока они соответствуют условию в predicate. Выбранные элементы возвращаются в виде потока. Промежуточная операция.

Object[] toArray() : возвращает массив из элементов потока. Терминальная операция.

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

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

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

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

Источник

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

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

какие типы операций есть в стримах. Смотреть фото какие типы операций есть в стримах. Смотреть картинку какие типы операций есть в стримах. Картинка про какие типы операций есть в стримах. Фото какие типы операций есть в стримах

какие типы операций есть в стримах. Смотреть фото какие типы операций есть в стримах. Смотреть картинку какие типы операций есть в стримах. Картинка про какие типы операций есть в стримах. Фото какие типы операций есть в стримах

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

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

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

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

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

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

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

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

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

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

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

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

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

какие типы операций есть в стримах. Смотреть фото какие типы операций есть в стримах. Смотреть картинку какие типы операций есть в стримах. Картинка про какие типы операций есть в стримах. Фото какие типы операций есть в стримах

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

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

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

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

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

Источник

Полное руководство по Java Stream API

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

API Java Stream не связан с Java InputStream и Java OutputStream Java IO. InputStream и OutputStream связаны с потоками байтов. Предназначен для обработки потоков объектов, а не байтов.

Java Stream – это компонент, способный выполнять внутреннюю итерацию своих элементов, то есть он может выполнять итерацию своих элементов сам.

Напротив, когда используются итерационные функции, мы должны сами реализовать итерацию элементов.

Потоковая обработка

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

Каждый слушатель получает возможность обрабатывать каждый элемент в потоке. Это называется потоковой обработкой.

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

какие типы операций есть в стримах. Смотреть фото какие типы операций есть в стримах. Смотреть картинку какие типы операций есть в стримах. Картинка про какие типы операций есть в стримах. Фото какие типы операций есть в стримах

Получение потока

Есть много способов получить поток Java. Один из самых распространенных способов получить поток – из Java Collection.

В этом примере сначала создали список Java, а затем добавили три строки. В итоге, в примере вызывается метод stream() для получения экземпляра потока.

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

Интерфейс Stream имеет выбор терминальных и не-терминальных операций.

Не-терминальная потоковая операция – это операция, которая добавляет слушателя в поток, не делая ничего другого.

Операция терминального потока – это операция, которая запускает внутреннюю итерацию элементов, вызывает всех слушателей и возвращает результат.

Вызов метода map() является не-терминальной операцией. Он просто преобразует каждый элемент в нижний регистр. Вызов метода count() является терминальной операцией. Этот вызов запускает внутреннюю итерацию, в результате чего каждый элемент преобразуется в нижний регистр.

Преобразование элементов в нижний регистр фактически не влияет на количество элементов.

Не-терминальные операции

Нетерминальные операции потока Java Stream API являются операциями, которые преобразовывают или фильтруют элементы в потоке. Когда добавляется не-терминальная операция в поток, в итоге мы получаем новый поток.

Этот вызов возвращает новый экземпляр Stream, представляющий исходный поток строк с применяемой операцией.

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

Обратим внимание, как второй вызов map() вызывается в потоке, возвращаемом первым вызовом map().

Многие нетерминальные операции Stream могут принимать Java Lambda Expression в качестве параметра. Это лямбда-выражение реализует Java functional interface, который подходит для данной не-терминальной операции.

Например, интерфейс Function или Predicate. Параметр метода не-терминальной операции обычно является функциональным интерфейсом, поэтому его также можно реализовать с помощью Java lambda expression.

filter()

filter() можно использовать для фильтрации элементов. Метод фильтра принимает Predicate, который вызывается для каждого элемента в потоке.

Если элемент должен быть включен в результирующий поток, Predicate должен вернуть значение “true”. Если элемент не должен быть включен, Predicate должны возвращать значение “false”.

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

flatMap()

Методы Java Stream flatMap() отображают один элемент на несколько элементов. Идея состоит в том, что мы «сплющиваем» каждый элемент из сложной структуры, состоящей из нескольких внутренних элементов, в «плоский» поток, состоящий только из этих внутренних элементов.

Например, представим, что у нас есть объект с вложенными объектами (дочерние объекты). Затем можно отобразить этот объект в «плоский» поток, состоящий из себя плюс его вложенные объекты – или только вложенные объекты.

Можно также отобразить поток списков элементов на сами элементы. Или сопоставить поток строк с потоком слов в этих строках – или с отдельными экземплярами символов в этих строках.

Этот пример flatMap() сначала создает список из 3 строк, содержащих названия книг. Затем получается поток для списка и вызывается flatMap().

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

Этот пример заканчивается вызовом forEach(). Предназначен только для запуска внутренней итерации и, следовательно, операции flat map. Если в цепочке Stream не было вызвано ни одной операции терминала, ничего бы не произошло. Никакого плоского картирования на самом деле не было бы.

distinct()

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

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

limit()

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

В этом примере сначала создается Stream, затем вызывается limit(), а затем вызывается forEach() с лямбда-выражением, которое выводит элементы в потоке. Только два первых элемента будут напечатаны из-за вызова limit(2).

Метод peek() – это не-терминальная операция, которая принимает Consumer(java.mutilfunction.Consumer) в качестве параметра. Consumer будет вызван для каждого элемента в потоке. Метод peek() возвращает новый поток, который содержит все элементы в исходном потоке.

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

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

Терминальные операции Java Stream обычно возвращают одно значение. Как только операция терминала вызывается в потоке, начинается итерация потока и любого из связанных потоков. По завершении итерации возвращается результат операции терминала.

Операция терминала обычно не возвращает новый экземпляр. Таким образом, как только вызывается терминальная операция в потоке, цепочка экземпляров Stream из не-терминальной операции заканчивается.

Поскольку count() возвращает long, цепочка нетерминальных операций заканчивается.

anyMatch()

Метод anyMatch() – это терминальная операция, которая принимает один Predicate в качестве параметра, запускает внутреннюю итерацию потока и применяет параметр Predicate к каждому элементу.

Если Predicate возвращает true для любого из элементов, метод anyMatch() возвращает true. Если ни один элемент не соответствует Predicate, anyMatch() вернет false.

В приведенном выше примере вызов вернет true, поскольку первый строковый элемент в потоке начинается с «One».

allMatch()

Метод Java Stream allMatch() является терминальной операцией, которая принимает один Predicate в качестве параметра, запускает внутреннюю итерацию элементов в потоке и применяет параметр Predicate к каждому элементу.

Если Predicate возвращает true для всех элементов в потоке, allMatch() вернет true. Если не все элементы соответствуют Predicate, метод allMatch() возвращает false.

В приведенном выше примере метод allMatch() вернет false, поскольку только одна из строк в Stream начинается с «One».

noneMatch()

noneMatch() является терминальной операцией, которая будет выполнять итерацию элементов в потоке и возвращать true или false в зависимости от того, соответствуют ли элементы в потоке Predicate, переданному noneMatch() в качестве параметра.

Метод noneMatch() вернет значение true, если ни один элемент не соответствует элементу Predicate, и значение false, если один или несколько элементов соответствуют.

Вот пример использования:

collect()

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

Метод collect() принимает в качестве параметра Collector (java.util.stream.Collector). Реализация Collector требует некоторого изучения интерфейса Collector.

К счастью, класс Java java.util.stream.Collectors содержит набор предварительно реализованных действий Collector, которые можно использовать для наиболее распространенных операций.

В приведенном выше примере использовалась реализация Collector, возвращаемая Collectors.toList(). Этот Collector просто собирает все элементы в потоке в стандартный список Java.

count()

Метод подсчета является терминальной операцией, которая подсчитывает элементы. Вот пример подсчета:

Сначала создается список строк, затем получается поток для этого списка, для него добавляется операция flatMap(), а затем заканчивается вызов метода count().

Метод count() запускает итерацию элементов в потоке, в результате чего строковые элементы разбиваются на слова в операции flatMap(), а затем подсчитываются. Окончательный результат, который будет выведен – 14.

findAny()

findAny() может найти отдельный элемент. Здесь все просто и понятно.

Обратим внимание, как метод findAny() возвращает Optional. Поток может быть пустым, поэтому элемент не может быть возвращен. Можно проверить, был ли элемент найден с помощью дополнительного метода isPresent().

FindFirst()

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

Можно проверить, содержит ли возвращаемый Optional элемент через его метод isPresent().

forEach()

forEach() является терминальной операцией, которая запускает внутреннюю итерацию элементов и применяет Consumer (java.util.function.Consumer) к каждому элементу в стриме.

min() является терминальной операцией, которая возвращает наименьший элемент в потоке. Наименьший элемент, определяется реализацией Comparator, которую мы передаем методу min().

Обратим внимание, как метод min() возвращает необязательный параметр, который может содержать или не содержать результат. Если поток пустой, дополнительный метод get() генерирует исключение NoSuchElementException.

max() возвращает самый большой элемент в потоке. Наибольший элемент определяется реализацией Comparator, которую мы передаем методу max().

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

reduce()

reduce() может свести все элементы в потоке к одному элементу. Посмотрите на реализацию:

Обратим внимание на необязательный параметр, возвращаемый методом reduce(). Этот необязательный параметр содержит значение (если оно есть), возвращаемое лямбда-выражением, переданным методу reduce(). Мы получаем значение, вызывая метод Optionalget().

toArray()

Метод Java Stream toArray() является терминальной операцией, которая запускает внутреннюю итерацию элементов в потоке и возвращает массив Object, содержащий все элементы.

Конкатенация потоков в Java

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

Недостатки Java Stream API

По сравнению с другими API потоковой передачи данных, такими как Apache Kafka Streams API, у Java Stream API есть небольшие недостатки. Они не являются очень важными, но их полезно иметь в виду.

Пакетный, но не потоковый

Несмотря на свое название, Java Stream API не является в действительности API потоковой обработки. Терминальные операции возвращают конечный результат итерации по всем элементам в потоке и предоставляют не-терминальные и терминальные операции элементам. Результат операции терминала возвращается после обработки последнего элемента в потоке.

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

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

Цепь, а не график

Можно добавить только одну не-терминальную операцию в Stream, что приведет к созданию нового объекта. Можно добавить еще одну не-терминальную операцию к результирующему объекту Stream, но не к первому. Результирующая структура нетерминальных экземпляров стрима образует цепочку.

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

Таким образом, каждый слушатель (не-терминальная операция) обычно может действовать как сам поток, который другие слушатели могут прослушивать результаты. Так устроен Apache Kafka Streams.

Чтобы легко поддерживать операции терминала, должна быть одна, последняя операция, из которой возвращается конечный результат. API обработки потоков на основе графика может вместо этого поддерживать «примерную» операцию, в которой у каждого узла в графике обработки потоков запрашивается любое значение, которое он может содержать внутри (например, сумма), если таковые имеются (чисто преобразовывающие узлы слушателя не будут иметь никакого внутреннего состояния ).

Внутренняя, а не внешняя итерация

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

Некоторые API обработки потоков на основе графиков также предназначены для того, чтобы скрыть итерацию элементов от пользователя API (например, Apache Kafka Streams и RxJava).

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

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

Средняя оценка / 5. Количество голосов:

Или поделись статьей

Видим, что вы не нашли ответ на свой вопрос.

Источник

Перевод руководства по 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 не будет опубликован. Обязательные поля помечены *