Stream api что это
Немного о Stream API(Java 8)
Небольшая статья с примерами использования Stream API в Java8, которая, надеюсь, поможет начинающим пользователям освоить и использовать функционал.
Итак, что такое Stream API в Java8? «Package java.util.stream» — «Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections». Попробую дать свой вариант перевода, фактически это — поддержка функционального стиля операций над потоками, такими как обработка и «свёртка» обработанных данных.
«Stream operations are divided into intermediate and terminal operations, and are combined to form stream pipelines. A stream pipeline consists of a source (such as a Collection, an array, a generator function, or an I/O channel); followed by zero or more intermediate operations such as Stream.filter or Stream.map; and a terminal operation such as Stream.forEach or Stream.reduce» — описание с сайта.
Попробуем разобраться в этом определении. Авторы говорят нам о наличии промежуточных и конечных операций, которые объедены в форму конвейеров. Потоковые конвейеры содержат источник (например, коллекции и т.п.) за которым следуют промежуточные и конечные операции и приводятся их примеры. Тут стоит заметить, что все промежуточные операции над потоками — ленивые(LAZY). Они не будут исполнены, пока не будет вызвана терминальная (конечная) операция.
Еще одна интересная особенность, это – наличие parallelStream(). Данные возможности я использую для улучшения производительности при обработке больших объемов данных. Параллельные потоки позволят ускорить выполнение некоторых видов операций. Я использую данную возможность, когда знаю, что коллекция достаточно большая для обработки ее в «ForkJoin» варианте. Подробнее про ForkJoin читайте в предыдущей статье на эту тему — «Java 8 в параллель. Учимся создавать подзадачи и контролировать их выполнение».
Закончим с теоретической частью и перейдем к несложным примерам.
Пример показывает нахождение максимального и минимального значения из коллекции.
Немного усложним пример и добавим исключения (в виде null) при максимального значения в пример №2.
Усложним примеры. Создадим коллекцию «спортивный лагерь», состоящую из полей «Имя» и «Количество дней в спортивном лагере». Сам пример создания класса ниже.
А теперь примеры работы с новыми данными:
В примере было найдено имя, Ирина, которая будет находиться в лагере всех дольше.
Преобразуем пример и создадим ситуацию, когда у нас вкралась ошибка, и одна из записей null в имени.
В этом случае вы получите результат, равный «Name=null».Согласитесь, что мы хотели не этого.Немного изменим поиск по коллекции на новый вариант.
Полученный результат, «Ira» — верен.
В примерах показано нам нахождение минимальных и максимальных значений по коллекциям с небольшими дополнениями в виде исключения null значений.
Как мы говорили доступные методы можно разделить на две большие группы промежуточные операции и конечные. Авторы могут называть их различно, например, вариант названия конвейерные и терминальные методы употребляется в литературе и статьях. При работе с методами существует одна конструктивная особенность, вы можете «накидывать» множество промежуточных операций, в конце производя вызов одного терминального метода.
В новом примере добавим сортировку и вывод определенного элемента, например, добавим фильтр по именам с встречающимся «Ivan» и произведем подсчет таких элементов (исключим null значения).
Добавив в коллекцию new SportsCamp(«Ivan», 17), получим результат равный «countName=2». Нашли две записи.
В данных примерах использовалось создание стрима из коллекции, доступны и другие варианты, например, создание стрима из требуемых значений, например, Stream streamFromValues = Stream.of(«test1», «test2», «test3»), возможны и другие варианты.
Как говорилось выше, у пользователей есть возможность использовать «обработку» используя parallelStream().
Немного изменив пример, получим новый вариант реализации:
Особенность этого варианта состоит в реализации параллельного стрима. Хочется обратить внимание, что parallelStream() оправданно использовать на мощных серверах(многоядерных) для больших коллекций. Я не даю четкого определения и точного размера коллекций, т.к. очень много параметров необходимо выявить и просчитать. Часто только тестирование может показать вам увеличение производительность.
Мы немного познакомились с простыми операциями, поняли отличие между конвейерными и терминальными операциями, попробовали и те и другие. А теперь давайте посмотрим примеры более сложных операций, например, collect и Map, Flat и Reduce.
Еще раз заглянем в официальную документацию документацию и попробуем реализовать свои примеры.
В новом примере попробуем преобразовать одну коллекцию в другую, по именам начинающимся с «I» и запишем это в List.
Результат будет равен трём. Тут нужно обратить внимание, что порядок указания исключения null элементов значим.
Обратите внимание, что Collectors обладает массой возможностей, включая вывод среднего значения или информации со статистикой. Как пример, попробуем соединить данные, вот так:
Java Stream API. Копилка рецептов
Если вы не любите стримы, возможно, вы пока не умеете их готовить 🙂 Приглашаем поучиться.
В этой статье почти нет теории, зато много практики и кода. Разберём семь типичных ситуаций, когда стримы бывают полезны. Сравним решения с классическими императивными реализациями.
Stream API — что это вообще такое
Это способ работать со структурами данных Java, чаще всего коллекциями, в стиле функциональных языков программирования.
О началах функционального программирования и лямбдах в Java читайте здесь.
Стрим — это объект для универсальной работы с данными. И это вовсе не какая-то новая структура данных, он использует существующие коллекции для получения новых элементов.
Затем к данным применяются методы. В интерфейсе Stream их множество. Каждый выполняет одну из типичных операций с коллекцией: отсортировать, перегруппировать, отфильтровать. Мы разберём некоторые из этих методов дальше.
Думайте о стриме как о потоке данных, а о цепочке вызовов методов — как о конвейере.
Каждый промежуточный метод получает на вход результат выполнения с предыдущего этапа (стрим), отвечает только за свою часть работы и возвращает стрим.
Последний (терминальный) метод либо не возвращает значения ( void), либо возвращает результат иного, нежели стрим, типа.
Преимущества
Стримы избавляют программистов от написания стереотипного кода всякий раз, когда нужно сделать что-то с набором элементов. То есть благодаря стримам не приходится думать о деталях реализации.
Есть и другие плюсы:
А теперь, когда вы почти поверили, что стримы — это хорошо, перейдём к практике.
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
Подготовим данные
Работу методов Java Stream API покажем на примере офлайновой библиотеки. Для каждой книги библиотечного фонда известны автор, название и год издания.
Для читателя библиотеки будем хранить ФИО и электронный адрес. Каждый читатель может взять в библиотеке одну или несколько книг — их тоже сохраним.
Ещё нам понадобится флаг читательского согласия на уведомления по электронной почте. Рассылки организуют сотрудники библиотеки: напоминают о сроке возврата книг, сообщают новости.
Java 8 Stream API: шпаргалка для программиста
Обработка данных — стандартная задача при разработке. Раньше для этого приходилось использовать циклы или рекурсивные функции. С появлением в Java 8 Stream API процесс обработки данных значительно ускорился. Этот инструмент языка позволяет описать, как нужно обработать данные, кратко и емко.
Что такое Java Stream API
Это новый инструмент языка Java, который позволяет использовать функциональный стиль при работе с разными структурами данных.
Для начала стриму нужен источник, из которого он будет получать объекты. Чаще всего это коллекции, но не всегда. Например, можно взять в качестве источника генератор, у которого заданы правила создания объектов.
Данные в стриме обрабатываются на промежуточных операциях. Например: мы можем отфильтровать данные, пропустить несколько элементов, ограничить выборку, выполнить сортировку. Затем выполняется терминальная операция. Она поглощает данные и выдает результат.
Stream на примере простой задачи
Для наглядности посмотрим на примере использование стримов в сравнении со старым решением аналогичной задачи.
Задача — найти сумму нечетных чисел в коллекции.
Решение с методами стрима:
Здесь мы видим функциональный стиль. Без стримов эту же задачу приходится решать через использование цикла:
Да, на первый взгляд цикл выглядит более понятным. Но это вопрос опыта взаимодействия со стримами. Очень быстро привыкаешь к тому, что можно обрабатывать данные без использования циклов.
Преимущества Stream
Благодаря стримам больше не нужно писать стереотипный код каждый раз, когда приходится что-то делать с данными: сортировать, фильтровать, преобразовывать. Разработчики меньше думают о стандартной реализации и больше времени уделяют более сложным вещам.
Еще несколько преимуществ стримов:
Даже сложные операции по обработке данных благодаря Stream API выглядят лаконично и понятно. В общем, писать становится удобнее, а читать — проще.
Как создавать стримы
В таблице ниже — основные способы создания стримов.
Источник | Способ | Пример |
Коллекция | collection.stream() | Collection collection = Arrays.asList(«f5», «b6», «z7»); Stream collectionS = collection.stream(); |
Значения | Stream.of(v1,… vN) | Stream valuesS = Stream.of(«f5», «b6», «z7»); |
Примитивы | IntStream.of(1, … N) | IntStream intS = IntStream.of(9, 8, 7); |
DoubleStream.of(1.1, … N) | DoubleStream doubleS = DoubleStream.of(2.4, 8.9); | |
Массив | Arrays.stream(arr) | String[] arr = <"f5","b6","z7">; Stream arrS = Arrays.stream(arr); |
Файл — каждая новая строка становится элементом | Files.lines(file_path) | Stream fromFileS = Files.lines(Paths.get(«doc.txt»)) |
Stream.builder | Stream.builder().add(. ). build() | Stream.builder().add(«f5»).add(«b6»).build() |
Стримы можно создавать не только из файлов, но и из списка объектов какой-либо директории или файлов, находящихся в какой-либо части дерева файловой системы.
В Stream.iterate мы задаем начальное значение, а также указываем, как будем получать следующее, используя предыдущий результат:
Stream.generate позволяет бесконечно генерировать постоянные и случайные значения, которые соответствуют указанному выражению.
Если хотите узнать больше об этих и других способах, читайте документацию Stream.
Методы стримов
В Java 8 Stream API доступны методы двух видов — конвейерные и терминальные. Кроме них можно выделить ряд спецметодов для работы с числовыми стримами и несколько методов для проверки параллельности/последовательности. Но это формальное разделение.
Конвейерных методов в стриме может быть много. Терминальный метод — только один. После его выполнения стрим завершается.
Пока вы не вызвали терминальный метод, ничего не происходит. Все потому, что конвейерные методы ленятся. Это значит, что они обрабатывают данные и ждут команды, чтобы передать их терминальному методу.
Конвейерные
Терминальные
Вот несколько интересных примеров:
Методы числовых стримов
Это специальные методы, которые работают только со стримами с числовыми примитивами.
Еще несколько методов
Напоследок посмотрим еще несколько полезных методов, которые помогают управлять последовательными и параллельными стримами — как минимум быстро их определять.
Метод | Что сделает | Использование |
isParallel | скажет, параллельный стрим или нет | someStream.isParallel() |
parallel | сделает стрим параллельным или вернет сам себя | someStream = stream.parallel() |
sequential | сделает стрим последовательным или вернет сам себя | someStream = stream.sequential() |
Не рекомендуется применять параллельность для выполнения долгих операций (например, извлечения данных из базы), потому что все стримы работают с общим пулом. Долгие операции могут остановить работу всех параллельных стримов в Java Virtual Machine из-за того, что в пуле не останется доступных потоков.
Чтобы избежать такой проблемы, используйте параллельные стримы только для коротких операций, выполнение которых занимает миллисекунды, а не секунды и тем более минуты.
В Stream API по умолчанию скрыта работа с потоконебезопасными коллекциями, разделение на части и объединение элементов. Это отличное решение. Разработчику остается только выбирать нужные методы и следить за тем, чтобы не было зависимостей от внешних факторов.
Решение задач с помощью Stream API
Посчитаем, сколько раз объект « High » встречается в коллекции:
А теперь посмотрим, какой элемент в коллекции находится на первом месте. Если мы получили пустую коллекцию, то пусть возвращается 0 :
Благодаря методам filter и findFirst можно находить элементы, равные заданным в условии:
collection.stream().skip(collection.size() — 1).findFirst().orElse(«0») // Highload
С помощью метода skip можно искать элементы по порядку. Например, пропустить первый и вывести второй:
collection.stream().skip(1).limit(2).toArray()// [High, Load]
С максимальным значением тоже все очень просто:
Первая задача — отсортировать строки в алфавитном порядке и добавить их в массив:
collection.stream().sorted().collect(Collectors.toList()) // [f2, f4, f4, f10, f15]
А вот чуть более интересное задание — нужно выполнить сортировку в обратном алфавитному порядке и удалить дубликаты. В массиве должны оказаться только уникальные значения:
Здесь мы используем не только sorted для сортировки, но и метод distinct для удаления неуникальных значений при обработке коллекции.
Задачи про группу студентов
Теперь давайте посмотрим чуть более комплексные, взрослые задачи. Например, у нас есть коллекция, которая имеет следующий вид:
Сначала создадим коллекцию студентов и опишем их:
Теперь мы можем использовать методы стримов для обработки этой коллекции. Посчитаем средний возраст, используя метод average :
Получилась немного странная группа студентов мужского пола, но средний возраст вполне себе студенческий. Что мы здесь сделали:
Теперь давайте посмотрим, кому из наших студентов грозит получение повестки в этом году при условии, что призывной возраст установлен в диапазоне от 18 до 27 лет.
Задачи на поиск в строке
Вот как будет выглядеть код этой программы:
Программа предлагает ввести имена сотрудников. Все они сохраняются в массив ALL без предварительной обработки. Чтобы остановить ввод имен, нужно ввести пустую строку.
Сначала на экране выведется массив со всеми введенными именами. Чтобы отфильтровать их, нужно добавить условие. В нашем случае это будет первая буква — например, ‘ a ‘.
Заключение
Stream в Java дает разработчикам удобные инструменты для обработки данных в коллекциях. Методы позволяют проще обрабатывать объекты и писать меньше кода.
Но стрим — не серебряная пуля. Опытные разработчики собрали несколько советов по их использованию:
Перевод руководства по 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. Чувствуйте себя свободно, создавая ответвление в репозитории.