Stream API
Начиная с JDK 8 в Java появился новый API — Stream API. Его задача — упростить работу с наборами данных, в частности, упростить операции фильтрации, сортировки и другие манипуляции с данными. Вся основная функциональность данного API сосредоточена в пакете java.util.stream .
Ключевым понятием в Stream API является поток данных . Вообще сам термин «поток» довольно перегружен в программировании в целом и в Java в частности. В одной из предыдущих глав рассматривалась работа с символьными и байтовыми потоками при чтении-записи файлов. Применительно к Stream API поток представляет канал передачи данных из источника данных. Причем в качестве источника могут выступать как файлы, так и массивы и коллекции.
Одной из отличительных черт Stream API является применение лямбда-выражений, которые позволяют значительно сократить запись выполняемых действий.
При ближайшем рассмотрении мы можем найти в других технологиях программирования аналоги подобного API. В частности, в языке C# некоторым аналогом Stream API будет технология LINQ.
Рассмотрим простейший пример. Допустим, у нас есть задача: найти в массиве количество всех чисел, которые больше 0. До JDK 8 мы бы могли написать что-то наподобие следующего:
int[] numbers = ; int count=0; for(int i:numbers) < if(i >0) count++; > System.out.println(count);
Теперь применим Stream API:
import java.util.stream.*; //. long count = IntStream.of(-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5).filter(w -> w > 0).count(); System.out.println(count);
Теперь вместо цикла и кучи условных конструкций, которые мы бы использовали до JDK 8, мы можем записать цепочку методов, которые будут выполнять те же действия.
При работе со Stream API важно понимать, что все операции с потоками бывают либо терминальными (terminal) , либо промежуточными (intermediate) . Промежуточные операции возвращают трансформированный поток. Например, выше в примере метод filter принимал поток чисел и возвращал уже преобразованный поток, в котором только числа больше 0. К возвращенному потоку также можно применить ряд промежуточных операций.
Конечные или терминальные операции возвращают конкретный результат. Например, в примере выше метод count() представляет терминальную операцию и возвращает число. После этого никаких промежуточных операций естественно применять нельзя.
Все потоки производят вычисления, в том числе в промежуточных операциях, только тогда, когда к ним применяется терминальная операция. То есть в данном случае применяется отложенное выполнение.
В основе Stream API лежит интерфейс BaseStream . Его полное определение:
interface BaseStream>
Здесь параметр T означает тип данных в потоке, а S — тип потока, который наследуется от интерфейса BaseStream.
BaseStream определяет базовый функционал для работы с потоками, которые реализуется через его методы:
- void close() : закрывает поток
- boolean isParallel() : возвращает true, если поток является параллельным
- Iterator iterator() : возвращает ссылку на итератор потока
- Spliterator spliterator() : возвращает ссылку на сплитератор потока
- S parallel() : возвращает параллельный поток (параллельные потоки могут задействовать несколько ядер процессора в многоядерных архитектурах)
- S sequential() : возвращает последовательный поток
- S unordered() : возвращает неупорядоченный поток
От интерфейса BaseStream наследуется ряд интерфейсов, предназначенных для создания конкретных потоков:
- Stream : используется для потоков данных, представляющих любой ссылочный тип
- IntStream : используется для потоков с типом данных int
- DoubleStream : используется для потоков с типом данных double
- LongStream : используется для потоков с типом данных long
При работе с потоками, которые представляют определенный примитивный тип — double, int, long проще использовать интерфейсы DoubleStream, IntStream, LongStream. Но в большинстве случаев, как правило, работа происходит с более сложными данными, для которых предназначен интерфейс Stream . Рассмотрим некоторые его методы:
- boolean allMatch(Predicate predicate) : возвращает true, если все элементы потока удовлетворяют условию в предикате. Терминальная операция
- boolean anyMatch(Predicate predicate) : возвращает true, если хоть один элемент потока удовлетворяют условию в предикате. Терминальная операция
- R collect(Collector collector) : добавляет элементы в неизменяемый контейнер с типом R. T представляет тип данных из вызывающего потока, а A — тип данных в контейнере. Терминальная операция
- 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 на простых примерах

Стримы и коллекции чем-то похожи друг на друга, но у них разное назначение. Коллекции обеспечивают эффективный доступ к одиночным объектам, а стримы, наоборот, для прямого доступа и обработки отдельных элементов не используются. Стримы предназначены для параллельных и последовательных агрегаций, выполняемых через цепочку методов.
Давайте сразу перейдем к примерам.
Для начала создадим класс User — основной класс для наших экспериментов.
import lombok.AllArgsConstructor; import lombok.Data; import lombok.ToString; @Data @AllArgsConstructor @ToString public class User
Далее напишем класс Sample , в котором будем запускать наш код.
import java.util.Arrays; import java.util.List; public class Sample < private final ListuserList = Arrays.asList( new User(1, "Michael", "Robert", 37, "TR"), new User(2, "Mary", "Patricia", 11, "EN"), new User(3, "John", "Michael", 7, "FR"), new User(4, "Jennifer", "Linda", 77, "TR"), new User(5, "William", "Elizabeth", 23, "US"), new User(6, "Sue", "Jackson", 11, "IT"), new User(7, "Michael", "Tommy", 37, "EN") ); public static void main(String. args) < Sample sample = new Sample(); >>
1. Перебор всех элементов userList с помощью forEach() и вывод их в консоль.
public static void main(String. args) < Sample sample = new Sample(); sample.test1(); >private void test1()
Test 1 User(id=1, firstName=Michael, lastName=Robert, age=37, nationality=TR) User(id=2, firstName=Mary, lastName=Patricia, age=11, nationality=EN) User(id=3, firstName=John, lastName=Michael, age=7, nationality=FR) User(id=4, firstName=Jennifer, lastName=Linda, age=77, nationality=TR) User(id=5, firstName=William, lastName=Elizabeth, age=23, nationality=US) User(id=6, firstName=Sue, lastName=Jackson, age=11, nationality=IT) User(id=7, firstName=Michael, lastName=Tommy, age=37, nationality=EN)
Так как userList — это ArrayList , то элементы выводятся в консоль в порядке добавления их в список.
2. Перебор всех элементов с выполнением некоторой операции над каждым элементом списка и вывод их на консоль.
private void test2() < System.out.println("Test 2"); userList.stream() .map(u ->< return new User( u.getId(), "X " + u.getFirstName(), "Y " + u.getLastName(), u.getAge() + 10), u.getNationality()); >) .collect(Collectors.toList()) .forEach(System.out::println); >
Test 2 User(id=1, firstName=X Michael, lastName=Y Robert, age=47, nationality=TR) User(id=2, firstName=X Mary, lastName=Y Patricia, age=21, nationality=EN) User(id=3, firstName=X John, lastName=Y Michael, age=17, nationality=FR) User(id=4, firstName=X Jennifer, lastName=Y Linda, age=87, nationality=TR) User(id=5, firstName=X William, lastName=Y Elizabeth, age=33, nationality=US) User(id=6, firstName=X Sue, lastName=Y Jackson, age=21, nationality=IT) User(id=7, firstName=X Michael, lastName=Y Tommy, age=47, nationality=EN)
3. Сортировка списка по свойству age .
private void test3()
Test 3 User(id=3, firstName=John, lastName=Michael, age=7, nationality=FR) User(id=2, firstName=Mary, lastName=Patricia, age=11, nationality=EN) User(id=6, firstName=Sue, lastName=Jackson, age=11, nationality=IT) User(id=5, firstName=William, lastName=Elizabeth, age=23, nationality=US) User(id=1, firstName=Michael, lastName=Robert, age=37, nationality=TR) User(id=7, firstName=Michael, lastName=Tommy, age=37, nationality=EN) User(id=4, firstName=Jennifer, lastName=Linda, age=77, nationality=TR)
4. Сортировка списка по нескольким свойствам: age , firstName , lastName .
private void test4()
Test 4 User(id=3, firstName=John, lastName=Michael, age=7, nationality=FR) User(id=2, firstName=Mary, lastName=Patricia, age=11, nationality=EN) User(id=6, firstName=Sue, lastName=Jackson, age=11, nationality=IT) User(id=5, firstName=William, lastName=Elizabeth, age=23, nationality=US) User(id=1, firstName=Michael, lastName=Robert, age=37, nationality=TR) User(id=7, firstName=Michael, lastName=Tommy, age=37, nationality=EN) User(id=4, firstName=Jennifer, lastName=Linda, age=77, nationality=TR)
5. Вычисление среднего возраста ( age ) и максимальной длины firstName .
private void test5() < System.out.println("Test 5"); double averageAge = userList.stream() .mapToInt(User::getAge) .summaryStatistics() .getAverage(); System.out.print("averageAge: " + averageAge); int maxFirstNameLenght = userList.stream() .mapToInt((value) ->< return value.getFirstName().length(); >) .summaryStatistics() .getMax(); System.out.println(" maxFirstNameLenght: " + maxFirstNameLenght); >
Test 5 averageAge: 29.0 maxFirstNameLenght: 8
6. Проверка, что у всех User возраст ( age ) больше 6.
private void test6() < System.out.println("Test 6"); boolean isAllAgesGreaterThan6 = userList.stream() .allMatch(user ->user.getAge() > 6); System.out.println("isAllAgesGreaterThan6: " + isAllAgesGreaterThan6); >
Test 6 isAllAgesGreaterThan6: true
7. Проверка, есть ли кто-то с firstName , начинающийся с символа S .
private void test7() < System.out.println("Test 7"); boolean isFirstCharS = userList.stream() .anyMatch(user ->user.getFirstName().charAt(0) == 'S'); System.out.println("isFirstCharS " + isFirstCharS); >
Test 7 isFirstCharS: true
8. Преобразование одной коллекцию в другую.
private void test8() < System.out.println("Test 8"); Listlist = userList.stream() .collect(Collectors.toList()); Set set = userList.stream() .collect(Collectors.toSet()); List linkedList = userList.stream() .collect(Collectors.toCollection(LinkedList::new)); Map map = userList.stream() .collect(Collectors.toMap(user -> user.getId(), user -> user)); >
9. Количество разных национальностей (nationality).
private void test9()
Test 9 countDifferentNationalites: 5
10. User старше 10 лет, у которых первый символ firstName не равен M .
private void test10() < System.out.println("Test 10"); userList.stream() .filter(p ->(p.getFirstName().charAt(0) != 'M')) .filter(p -> (p.getAge() > 10)) .collect(Collectors.toList()) .forEach(System.out::println); >
Test 10 User(id=4, firstName=Jennifer, lastName=Linda, age=77, nationality=TR) User(id=5, firstName=William, lastName=Elizabeth, age=23, nationality=US) User(id=6, firstName=Sue, lastName=Jackson, age=11, nationality=IT)
Резюмируя, Java Stream — это не структура хранения данных. Стримы передают элементы из источника (какой-либо структуры данных) через конвейер операций и возвращают некоторое значение, не модифицируя источник.
Все примеры находятся в репозитории GitHub.
Приглашаем всех желающих на открытый урок «Переопределение, скрытие, передекларация». Будет рассмотрено переопределение и скрытие методов в Java, а также передекларация и скрытие переменных. Познакомимся с четырьмя правилами, а потом ещё и с пятым. Регистрация здесь.
- Блог компании OTUS
- Программирование
- Java
Глубокое погружение в Stream API Java: Понимание и Применение
В этой статье мы погрузимся в мир Stream API, узнаем, что это такое и как этим пользоваться, разберем реальные примеры и советы по лучшим практикам.
7 июля 2023 · 18 минуты на чтение

Версия Java 8 принесла множество новшеств, которые значительно упростили обработку и манипулирование данными. Одним из таких нововведений стал Stream API — эффективный инструмент для обработки коллекций в функциональном стиле.
Спонсор поста
Зачем нужен Stream API?
В прошлом, при работе с коллекциями в Java, разработчики часто прибегали к циклам и условным операторам для фильтрации, преобразования или агрегации данных. Этот подход обычно требовал большого объема кода, был трудночитаемым и подвержен ошибкам. Возьмите, например, этот код:
public void printSpecies(List seaCreatures) < SetspeciesSet = new HashSet<>(); for (SeaCreature sc : seaCreatures) < if (sc.getWeight() >= 10) speciesSet.add(sc.getSpecies()); > List sortedSpecies = new ArrayList<>(speciesSet); Collections.sort(sortedSpecies, new Comparator() < public int compare (Species a, Species b) < return Integer.compare(a.getPopulation(), b.getPopulation()); >>); for (Species s : sortedSpecies) System.out.println(s.getName()); >
Он выглядит довольно громоздким, несмотря на то, что не выполняет ничего сложного. Теперь взгляните на тот же пример, но с использованием Stream API:
public void printSpecies(List seaCreatures) < seaCreatures.stream() .filter(sc ->sc.getWeight() >= 10) .map(SeaCreature::getSpecies) .distinct() .sorted(Comparator.comparing(Species::getPopulation)) .map(Species::getName) .forEach(System.out::println); >
С приходом Stream API картина радикально изменилась. Stream API обеспечивает функциональный стиль работы с данными, предлагая более компактный, выразительный и читаемый код, а также облегчая параллельное выполнение операций.
Основы Stream API
Stream API не предлагает решения для всех возможных сценариев обработки данных. Однако, большинство задач могут быть описаны следующим общим шаблоном:
- Источник данных.
- Выполнение преобразований.
- Сохранение результата в новую структуру данных.
Если ваша задача не соответствует этому шаблону, то, возможно, использование Stream API не будет оптимальным решением.
«Стримоз» головного мозга
Это условное заболевание может возникнуть у разработчиков, которые недавно узнали о существовании Stream API. Основной симптом — необузданное желание использовать Stream API для выполнения любых операций над коллекциями.
Пример из реального мира
Представьте, что Stream API — это конвейер на рыболовецком судне. Источник — это река, полная разнообразными морскими обитателями. Мы начинаем с этой «реки» и запускаем Stream, который можно сравнить с конвейером:

- Сначала мы используем filter() для отделения рыб от всех других морских существ. Это напоминает рыбаков, которые отбирают только нужные виды рыбы из всего множества разных созданий в реке.
- Затем, с помощью map() , мы преобразуем каждую рыбу в «упаковку с рыбой». Это подобно рыболовному сету, которое собирает рыбу и укладывает ее в контейнеры.
- В конце, с помощью collect() , мы складываем все упаковки с рыбой в «грузовик» для последующей транспортировки.
Все это происходит в рамках одного непрерывного процесса, или ‘потока’. Stream API позволяет нам обрабатывать каждый элемент коллекции эффективно и последовательно, подобно конвейеру на фабрике.
Компоненты Stream API
Stream API состоит из набора компонентов и концепций, которые работают вместе, чтобы обеспечить потоковую обработку данных.

- Источник (Source) — откуда приходят данные. Это может быть коллекция, массив, файл, генератор или любой другой источник данных.
- Операции — преобразовывают и/или обрабатывают данные.
- Поток (Stream) — последовательность элементов, подлежащих параллельной или последовательной обработке.
- Пайплайн (Pipeline) — последовательность операций в потоке, применяемых к данным.
- Терминал (Terminal) — место выхода данных из потока. Терминальная операция означает окончание обработки потока и возвращает результат.
Источники даных для потоков
Stream API способен работать с разнообразными источниками данных. Это могут быть коллекции, списки, наборы, массивы, строки, файлы или даже генераторы чисел. Все эти источники могут быть легко преобразованы в потоки для последующей обработки.
Важно отметить, что при выполнении Stream исходные данные не изменяются. В результате своей работы Stream создает новую структуру данных.

Spliterator используется в основе стримов в Java и играет важную роль при параллельной обработке данных, так как именно он отвечает за разделение данных на части для независимой обработки каждым потоком.
Методы Spliterator
Spliterator описывает 4 основных метода:
- long estimateSize() возвращает количество элементов.
- tryAdvance(Consumer) принимает функциональный интерфейс Consumer , который определяет действия, которые должны быть выполнены над текущим элементом.
- int characteristics() возвращает набор характеристик текущего сплитератора.
- Spliterator trySplit() пытается разделить текущий сплитератор на два. Если операция успешна, то возвращает новый сплитератор, и уменьшает размер исходного сплитератора. Если разделение не возможно, то возвращает null .
Характеристики Spliterator
Spliterator обладает специальными характеристиками, которые сообщают об особенностях источника данных, из которого он был создан. Эти характеристики помогают в оптимизации работы потока при выполнении терминальных операций. Например, нет смысла выполнять сортировку уже отсортированной коллекции.
- ORDERED : указывает, что элементы имеют определенный порядок.
- DISTINCT : указывает, что каждый элемент уникален. Определяется по equals() .
- SORTED : указывает, что элементы отсортированы.
- SIZED : указывает, что размер источника известен заранее.
- NONNULL : указывает, что ни один элемент не может быть null .
- IMMUTABLE : указывает, что элементы не могут быть модифицированы.
- CONCURRENT : указывает, что исходные данные могут быть модифицированы без воздействия на Spliterator .
- SUBSIZED : указывает, что размер разделенных Spliterator -ов также будет известен.
В зависимости от типа коллекции, из которой получен Spliterator , будут установлены разные характеристики. Например, для коллекции Collection будет установлен флаг SIZED , для Set добавится DISTINCT , а для SortedSet еще и SORTED .
Каждая операция может менять флаги характеристик. Это важно, поскольку каждый этап обработки данных будет знать об этих изменениях, что позволяет выполнить оптимальные действия. Например, операция map() сбросит флаги SORTED и DISTINCT , так как данные могут измениться, но всегда сохранит флаг SIZED , так как размер потока не изменяется при выполнении map() .
Параллельное выполнение
Stream API предоставляет возможность параллельной обработки данных, что может способствовать увеличению производительности на многоядерных процессорах. Однако необходимо учитывать, что параллельное выполнение может внести дополнительную сложность и накладные расходы, поэтому следует использовать его обдуманно.
Для выполнения потоков в параллельном режиме можно использовать методы parallelStream() или parallel() . Без явного вызова этих методов поток будет выполняться последовательно. Для разделения коллекции на части, которые могут быть обработаны отдельными потоками, Java использует метод Spliterator.trySplit() .
С точки зрения плана выполнения, параллельная обработка схожа с последовательной, за исключением одного основного отличия. Вместо одного набора связанных операций у нас будет несколько его копий, и каждый поток будет применять эти операции к своему сегменту элементов. После завершения обработки все результаты, полученные каждым потоком, объединяются в один общий результат.

Место расположение .parallel() в Pipline
Может возникнуть вопрос, влияет ли расположение метода .parallel() в пайплайне на поведение потока?

В действительности, место, где указан .parallel() , не имеет значения, поскольку это просто устанавливает характеристику CONCURRENT . В свою очередь, sequential() удаляет эту характеристику.
Еще один важный аспект, который следует помнить о параллельных потоках, заключается в том, что Java назначает каждый фрагмент работы потоку в общем ForkJoinPool , аналогично тому, как это происходит в CompletableFuture .
Что же такое Stream?
Центральной концепцией Stream API является потоковые операции, представляющие собой ряд последовательных действий, выполняемых над данными.
Основные свойства потоков:
- Декларативность: Потоки в Java описывают, что должно быть сделано, а не конкретный способ его выполнения.
- Ленивость: Это означает, что потоки не выполняют никакой работы, пока не будет вызвана терминальная операция.
- Одноразовость: После того как терминальная операция была вызвана на потоке, этот поток больше не может быть использован. Если необходимо применить другую операцию к данным, потребуется новый поток.
- Параллельность: Несмотря на то, что потоки в Java по умолчанию выполняются последовательно, их можно легко распараллелить.
Рандомный блок
Методы Stream
Теперь, когда мы знаем, для чего нужны потоки и как они устроены изнутри, давайте рассмотрим все возможные способы работы со Stream API.
Создание Stream
Есть несколько способов создать поток с использованием Stream API. Например, вы можете создать поток из коллекции с помощью метода stream() или из массива с помощью метода of() . Как только у вас есть поток, вы можете выполнять различные операции с помощью методов, предоставляемых Stream API.
От коллекции
Вы можете создать поток из любой коллекции Java, например, списка или множества, с помощью метода stream() .
Этим способом вы будете создавать 90% своих стримов.
Collection list = new ArrayList<>(); Stream stream = list.stream();
Из массива
Поток может быть создан из массива с помощью метода Arrays.stream() .
int[] numbers = ; Stream stream = Arrays.stream(numbers).boxed();
Из строки
Поток может быть создан из строки с помощью метода chars() , который возвращает IntStream .
String str = "Hello"; IntStream stream = str.chars();
Из файла
Поток может быть создан из строк файла с помощью метода Files.lines() .
Path path = Paths.get("file.txt"); Stream stream = Files.lines(path);
Stream из Iterator-а
Многие источники данных хорошо делятся на части, что позволяет использовать преимущества параллельной обработки. Однако такие источники, как Files.lines() , Files.find() , Files.walk() , Files.list() , BufferedReader().lines() , Pattern.splitAsStream() , создают вначале Iterator , который затем трансформируется в Spliterator .
Проблема в том, что Iterator не содержит информации о размере исходного набора данных. Тем временем, для эффективной работы, Spliterator предполагает наличие информации о размере. Без этой информации Spliterator не может эффективно разбивать данные на части, что может привести к снижению эффективности параллелизма или даже к его полному отсутствию.
Генерирование
Поток может быть создан с помощью метода Stream.generate(Supplier) . Supplier должен возвращать новое значение при каждом вызове.
Stream stream = Stream.generate(() -> new Random().nextInt());
Билдер
Поток может быть создан с помощью Stream.Builder .
Stream.Builder builder = Stream.builder(); builder.add(1); builder.add(2); builder.add(3); Stream stream = builder.build();
Промежуточные методы
Мы разобрались с большинством методов создания стрима. Теперь давайте рассмотрим методы для обработки элементов.
filter(Predicate)
Этот метод используется для создания нового потока, включающего только элементы, которые удовлетворяют определенному условию. В качестве аргумента метод принимает функциональный интерфейс Predicate , задающий условие фильтрации.
Вот пример использования метода filter() для создания нового потока, который включает только четные числа из списка целых чисел.
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Stream evenNumbersStream = numbers.stream() .filter(n -> n % 2 == 0); evenNumbersStream.forEach(System.out::println); // prints 2, 4, 6, 8, 10
ORDERED : обычно сохраняется.
DISTINCT , SORTED , NONNULL , IMMUTABLE , CONCURRENT : сохраняются, если были в исходном Spliterator .
SIZED , SUBSIZED : теряются, поскольку количество элементов после фильтрации неизвестно.
map(Function)
Метод map() принимает в качестве аргумента функциональный интерфейс Function , задающий преобразование, применяемое к каждому элементу. Возвращаемый поток содержит преобразованные элементы.
Метод map() возвращает новый поток. Он не изменяет исходный поток и коллекцию. Обычно он используется для выполнения операций, таких как преобразование элементов из одного типа в другой.
List words = Arrays.asList("apple", "banana", "orange", "peach"); Stream lengthsStream = words.stream() .map(String::length); lengthsStream.forEach(System.out::println); // prints 5, 6, 6, 5
В данном примере мы с помощью map() преобразовали строку в количество символов в строке, используя короткую запись лямбды ( String::length ), так называемую ссылку на метод.
ORDERED : обычно сохраняется.
DISTINCT , SORTED : могут быть потеряны.
SIZED , SUBSIZED : обычно сохраняются.
NONNULL : может быть потеряно.
IMMUTABLE , CONCURRENT : сохраняются.
flatMap()
Метод flatMap() используется для создания одного потока из множества потоков. Он принимает функцию в качестве аргумента, которая применяется к каждому элементу исходного потока. Эта функция принимает элемент исходного потока и возвращает новый поток.
List> listOfLists = Arrays.asList( Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6), Arrays.asList(7, 8, 9) ); Stream flattenedStream = listOfLists.stream() .flatMap(Collection::stream); flattenedStream.forEach(System.out::println); // prints 1, 2, 3, 4, 5, 6, 7, 8, 9
В данном примере мы начинаем со списка списков целых чисел и создаем поток с помощью метода stream() . Затем мы используем метод flatMap() для создания нового потока, включающего все целые числа из вложенных списков, путем применения метода stream() к каждому из вложенных списков. Наконец, мы используем метод forEach() для вывода каждого элемента нового потока.
Влияние на характеристики:
ORDERED : обычно сохраняется.
DISTINCT , SORTED : обычно теряются.
SIZED , SUBSIZED : теряются.
NONNULL : может быть потеряно.
IMMUTABLE , CONCURRENT : сохраняются.
Разница между map() и flatMap()
Функция map() преобразует элемент исходного потока из одного типа в другой. В отличие от этого, функция flatMap() позволяет получить новый поток из элементов коллекций, которые были внутри элементов первого потока.
distinct()
Метод distinct() возвращает новый поток, содержащий только уникальные элементы исходного потока. Дубликаты определяются на основе их естественного порядка или с использованием переданного компаратора.
Вот пример, в котором функция distinct() используется для удаления дубликатов из потока целых чисел:
List numbers = Arrays.asList(1, 2, 3, 2, 1, 4, 5, 3, 5); List uniqueNumbers = numbers.stream() .distinct() .collect(Collectors.toList()); System.out.println(uniqueNumbers); // prints [1, 2, 3, 4, 5]
ORDERED : обычно сохраняется.
DISTINCT : всегда устанавливается после операции.
SORTED : сохраняется, если исходный Spliterator был SORTED.
SIZED , SUBSIZED : теряются, поскольку количество уникальных элементов неизвестно.
NONNULL , IMMUTABLE , CONCURRENT : сохраняются.
limit(n)
Метод limit(n) возвращает новый поток, содержащий не более n элементов исходного потока. Если исходный поток содержит меньше n элементов, новый поток будет содержать все элементы исходного потока.
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List limitedNumbers = numbers.stream() .limit(5) .collect(Collectors.toList()); System.out.println(limitedNumbers); // prints [1, 2, 3, 4, 5]
ORDERED : сохраняется, если исходный Spliterator был упорядочен.
DISTINCT , SORTED , NONNULL , IMMUTABLE , CONCURRENT : сохраняются, если были в исходном Spliterator .
SIZED , SUBSIZED : могут быть установлены, если размер исходного стрима был известен и больше значения limit() , иначе могут быть потеряны.
skip(n)
Метод skip(n) возвращает новый поток, который содержит все элементы исходного потока, исключая первые n элементов. Если исходный поток содержит меньше n элементов, новый поток будет пустым.
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List skippedNumbers = numbers.stream() .skip(5) .collect(Collectors.toList()); System.out.println(skippedNumbers); // prints [6, 7, 8, 9, 10]
ORDERED : сохраняется, если исходный Spliterator был упорядочен.
DISTINCT , SORTED , NONNULL , IMMUTABLE , CONCURRENT : сохраняются, если были в исходном Spliterator .
SIZED , SUBSIZED : могут быть потеряны, если количество пропускаемых элементов неизвестно или исходный Spliterator не был SIZED или SUBSIZED . Если размер исходного стрима известен и больше значения skip, эти характеристики сохраняются.
sorted()
Метод sorted() создает новый поток, содержащий элементы исходного потока, отсортированные в порядке возрастания.
При вызове метода sorted() возвращается новый поток, содержащий те же элементы, что и исходный поток, но в отсортированном порядке.
List names = Arrays.asList("Alice", "Bob", "Charlie", "David"); List sortedNames = names.stream() .sorted() .collect(Collectors.toList()); System.out.println(sortedNames); // prints ["Alice", "Bob", "Charlie", "David"]
Если элементы исходного потока не реализуют интерфейс Comparable , может возникнуть исключение ClassCastException . Чтобы избежать этого, можно предоставить собственный компаратор в качестве аргумента метода sorted() .
ORDERED : всегда устанавливается после операции sorted() , так как элементы теперь упорядочены в соответствии с естественным порядком или порядком, определенным компаратором.
DISTINCT : сохраняется, если был в исходном Spliterator .
SORTED : всегда устанавливается.
SIZED , SUBSIZED : сохраняются, если были в исходном Spliterator .
NONNULL : сохраняется, если был в исходном Spliterator .
IMMUTABLE , CONCURRENT : сохраняются, если были в исходном Spliterator .
takeWhile(Predicate)
Метод takeWhile() создает новый поток, содержащий элементы исходного потока до тех пор, пока они удовлетворяют указанному условию. Если первый элемент потока не соответствует предикату, новый поток будет пустым.
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List takenNumbers = numbers.stream() .takeWhile(n -> n < 5) .collect(Collectors.toList()); System.out.println(takenNumbers); // prints [1, 2, 3, 4]
ORDERED : сохраняется, если исходный Spliterator был упорядочен.
DISTINCT , SORTED , NONNULL , IMMUTABLE , CONCURRENT : сохраняются, если были в исходном Spliterator.
SIZED , SUBSIZED : могут быть потеряны, поскольку количество элементов после takeWhile() неизвестно.
dropWhile(Predicate)
Метод dropWhile() возвращает новый поток, который включает все элементы исходного потока, начиная с первого элемента, не удовлетворяющего указанному условию. В момент, когда предикат возвращает false , все последующие элементы из исходного потока включаются в новый поток.
List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List droppedNumbers = numbers.stream() .dropWhile(n -> n < 5) .collect(Collectors.toList()); System.out.println(droppedNumbers); // prints [5, 6, 7, 8, 9, 10]
ORDERED : сохраняется, если исходный Spliterator был упорядочен.
DISTINCT , SORTED , NONNULL , IMMUTABLE , CONCURRENT : сохраняются, если были в исходном Spliterator.
SIZED , SUBSIZED : могут быть потеряны, поскольку количество элементов после takeWhile() неизвестно.
peek(Consumer)
Метод peek() создает новый поток, идентичный исходному, но с дополнительной операцией, применяемой к каждому элементу при его прохождении по конвейеру потока.
List numbers = Arrays.asList(1, 2, 3, 4, 5); numbers.stream() .peek(System.out::println) .collect(Collectors.toList());
В данном примере, метод peek() применяется к потоку чисел. Consumer , переданный в метод peek() , выводит каждый элемент на консоль. В процессе этого, каждый элемент, проходя по конвейеру потока, отображается на консоли, но сам поток остается неизменным.
Метод peek() удобен, когда необходимо выполнить дополнительные операции с элементами потока, например, для целей логирования, отладки или профилирования, не меняя при этом сами элементы. Но важно быть осторожным с его использованием, так как неправильное применение метода peek() может привести к нежелательным последствиям.
Поскольку peek() – это промежуточная операция, которая не предназначена для изменения элементов потока, непреднамеренные изменения могут вызвать непредсказуемые результаты при параллельном выполнении потока.
В общем случае, рекомендуется использовать peek() редко и, преимущественно, для отладки, а не как средство модификации элементов потока. Если требуется изменить элементы потока, предпочтительнее использовать метод map() .
На моей практике был всего 1 случай, когда реализовать по другому было невозможно и пришлось использовать peek() .
Терминальные методы
Как упоминалось ранее, терминальный метод запускает обработку всего потока. Рассмотрим основные из таких методов.
forEach(Consumer)
Этот метод не рекомендуется использовать в продакшене, так как он не возвращает результат. Это означает, что у него может быть только побочный эффект. Например, если мы начинаем собирать данные с помощью метода forEach() , а затем кто-то применяет параллельное выполнение к стриму, мы тут же столкнемся со всеми проблемами синхронизации.

Класс Collectors содержит набор статических методов-коллекторов, которые упрощают выполнение общих операций, таких как преобразование элементов в списки, множества и другие структуры данных.
Вот некоторые наиболее популярные методы класса Collectors :
- toList() : Этот метод возвращает коллектор, который накапливает входные элементы в новый List .
- toSet() : Этот метод возвращает коллектор, который накапливает входные элементы в новый Set .
- joining() : Возвращает коллектор, который объединяет элементы потока в единую строку.
- counting() : Возвращает коллектор, который подсчитывает количество элементов в потоке.
Вы можете быстро реализовать метод collect(Collector collector) для сбора элементов в какую-то конкретную структуру.
Stream >stream; List >list = stream.collect(Collectors.toList()); //Коллектор выше аналогичен данному коду list = stream.collect( () -> new ArrayList<>(), // определяем структуру (list, t) -> list.add(t), // определяем, как добавлять элементы (l1, l2) -> l1.addAll(l2) // и как объединять две структуры в одну );
reduce()
Метод reduce() применяется для комбинирования элементов потока в одно значение. Он отличается от метода collect() тем, что использует ассоциативную функцию, принимающую два значения и объединяющую их в одно. Например, метод reduce() можно использовать для суммирования чисел или для нахождения максимального или минимального числа.

Optional findAny()
Метод findAny() может оказаться полезным в ситуациях, когда вам нужно получить любой элемент из потока без конкретного предпочтения.
В отличие от findFirst() , который всегда возвращает первый найденный элемент потока, findAny() , при параллельном выполнении потока, может возвращать любой элемент, поскольку выбор элемента зависит от того, какой поток обработает его первым.
boolean anyMatch(Predicate)
Метод anyMatch(Predicate) используется для проверки, соответствует ли хотя бы один элемент потока указанному предикату.
boolean allMatch(Predicate)
Возвращает true , если все элементы потока удовлетворяют предикату.
Short-circuiting
Рассмотрим так называемые операции "короткого замыкания", которые прекращают обработку, как только находят нужный результат. Это значительно повышает производительность, особенно при работе с большими потоками данных. Примерами операций короткого замыкания могут служить методы anyMatch() , allMatch() , noneMatch() , findFirst() , findAny() .
Важно отметить, что поведение операций короткого замыкания может изменяться в зависимости от того, является ли поток параллельным или последовательным. Например, в параллельном потоке метод findAny() может вернуть любой элемент, удовлетворяющий условию, вместо первого попавшегося, как это происходит в последовательном потоке.
Продвинутые советы и использование
В этом разделе будут собраны различные продвинутые подходы для работы со Stream API.
Возвращать Stream вместо коллекций
Это позволит защитить вашу коллекцию внутри сущности, не позволяя ее модифицировать извне.
Так же потребитель вашего API сможет сам выбрать, какая коллекция ему нужна.

Группировка элементов
Чтобы сгруппировать данные по какому-нибудь признаку, нам надо использовать метод collect() и метод Collectors.groupingBy() .
Этот раздел "честно" позаимствован из этой статьи на хабре, чтобы сохранить эту шпаргалку у себя, если автор скроет статью или что-то случится с хабром.
Группировка списка рабочих по их должности (деление на списки)
Map> map1 = workers.stream() .collect(Collectors.groupingBy(Worker::getPosition));
Группировка списка рабочих по их должности (деление на множества)
Map> map2 = workers.stream() .collect( Collectors.groupingBy( Worker::getPosition, Collectors.toSet() ) );
Подсчет количества рабочих, занимаемых конкретную должность
Map map3 = workers.stream() .collect( Collectors.groupingBy( Worker::getPosition, Collectors.counting() ) );
Группировка списка рабочих по их должности, при этом нас интересуют только имена
Map> map4 = workers.stream() .collect( Collectors.groupingBy( Worker::getPosition, Collectors.mapping( Worker::getName, Collectors.toSet() ) ) );
Расчет средней зарплаты для данной должности
Map map5 = workers.stream() .collect( Collectors.groupingBy( Worker::getPosition, Collectors.averagingInt(Worker::getSalary) ) );
Группировка списка рабочих по их должности, рабочие представлены только именами единой строкой
Map map6 = workers.stream() .collect( Collectors.groupingBy( Worker::getPosition, Collectors.mapping( Worker::getName, Collectors.joining(", ", "") ) ) );
Группировка списка рабочих по их должности и по возрасту.
Map>> collect = workers.stream() .collect( Collectors.groupingBy( Worker::getPosition, Collectors.groupingBy(Worker::getAge) ) );
Заключение
В целом, Stream API в Java — это мощный инструмент для обработки данных, который может кардинально изменить ваш подход к программированию. Он позволяет организовывать код в читаемые и компактные последовательности операций, что делает его идеальным для работы с большими объемами данных.
Вместе с тем, важно помнить, что он не подходит для всех задач. Если ваша задача не соответствует шаблону "источник-преобразование-сбор", возможно, стоит обратиться к другим инструментам Java.
В любом случае, понимание и умение использовать Stream API является важным навыком для каждого разработчика на Java, и безусловно, этот инструмент заслуживает времени, уделенного на его изучение.
Что нужно знать о Java Stream API
Всем привет! В этой статье я хочу познакомить вас, на мой взгляд, с одним из самых значительных нововведений в Java со времен ее появления — это Java Stream API.
Что такое Java Stream API? Зачем? И какие дает преимущества?
Очень часто, когда мы пишем программу, нам нужно обрабатывать наши данные. Для обработки данных мы используем циклы либо рекурсивные функции для обхода наших данных.
Java Stream API был создан для того, чтобы помочь пользователям ускорить и упростить обработку данных. Сам по себе API предоставляет инструмент, который позволяет нам дать рецепт того как обрабатывать объекты.
Если проводить параллели с реальным миром, то давайте представим, что у нас есть некий завод по производству мебели под заказ.
Грузовые автомобили привозят бревна на завод. На данном заводе у нас есть люди которых мы обучили что-то делать с древесиной, чтобы из нее получилась мебель: они просматривают каждое бревно на предмет дефектов и отфильтровывают брак, распиливают бревна, обрабатывают доски, собирают при помощи гвоздей и клея и защищают готовую продукцию при помощи лака.
Последний элемент в этой цепи — покупатель, который приходит на завод и делает заказ.
Без покупателя нет смысла запускать все производство, поэтому весь процесс стартует во время запуска производства.
В мире Java такой завод называется Stream API. Этот API представляет собой библиотеку, которая помогает в функциональном стиле кратко, но емко описывать, как обработать данные.
Как и в примере про завод, у каждого стрима должен быть источник объектов. Этим источником информации чаще всего бывает коллекция, так как именно в них мы и храним наши данные, но это не обязательно — может быть и какой-то генератор, который генерирует объекты по заданному правилу, примеры мы рассмотрим позже.
В Java Stream API также предусмотрены промежуточные операции. Они выполняют роль рабочих. Операции описывают процесс обработки объектов.
В конце каждого стрима должна быть терминальная операция, которая должна поглотить все обработанные данные.
В примере про завод мы видели, что заказчик становится триггером начала производства и является последним звеном в работе завода — он забирает всю продукцию.
Рассмотрим простейший стрим. Создадим класс бревно и поместим несколько бревен в коллекцию:
class Log < String type; int count; // конструктор и гетеры опущены >List logs = List.of( new Log("Сибирская сосна", 10), new Log("Дуб монгольский", 30), new Log("Берёза карликовая", 5));
У коллекций есть метод stream() , который возвратит стрим для данного набора данных.
Stream stream = logs.stream();
Получив ссылку на стрим, мы можем начать обрабатывать поток наших данных.
Отфильтруем бревна, количество которых меньше 7 и оставим только те, которые не являются дубом. Выглядеть это будет так:
Stream filteredStream = stream.filter(x -> x.getCount() > 7) .filter(x -> !"Дуб монгольский".equalsIgnoreCase(x.getType()));
Мы добавили фильтры и получили стрим, в котором описан процесс обработки всех наших бревен. Теперь мы должны добавить к нему терминальную операцию, чтобы запустить поток данных из коллекции:
filteredStream.forEach(x -> System.out.println(x.getType()));
В этом примере конечная операция принимает оставшиеся элементы после фильтрации и распечатывает их. Стоит особо упомянуть, что второй раз вызвать терминальную операцию не получится — стрим является «одноразовым» объектом. Это сделано авторами библиотеки для того, чтобы можно было корректно обрабатывать данные, которые имеют ограниченное время жизни. Например, если обрабатывать пакеты из интернета, то данные в стрим могут попасть только один раз, поэтому повторный вызов теряет всякий смысл.
Как упоминалось ранее, создать источник данных можно разными способами. Рассмотрим самые популярные.
Способы создания источника данных
В начале пройдемся по методам объявленным в интерфейсе Stream.
Stream.of(). Метод принимает массив объектов и создает на их основе стрим.
Stream stringStream = Stream.of("asd", "aaa", "bbb");
Для создания пустого стрима существует метод:
Патерн строитель поддерживается библиотекой, потому получив объект строителя Stream.builder() мы можем сконструировать с помощью него новый стрим.
Если у нас есть два стрима, мы можем объеденить их в один вызвав метод:
Stream.concat(Stream.of("aaa", "bbb", "ccc"), Stream.of("111", "222", "333"))
В итоге мы получим стрим, в котором будет находится шесть элементов.
Стрим не обязательно должен поглощать какие-то данные, можно создать генератор, который будет поставлять в наш стрим с помощью метода generate()
Stream.generate(() -> Math.random()).forEach(System.out::println);
Так как генератор может бесконечно генерировать стрим и в примере выше мы получим бесконечный вывод на экран случайных значений, необходимо добавить промежуточную операцию limit(100) — она позволит ограничить стрим. С этими операциями мы познакомимся позже.
Аналогичную функциональность предоставляет класс Random. В нем уже есть методы которые создают стримы из случайных чисел.
new Random().ints() new Random().doubles() new Random().longs()
Тут стоить отметить, что порой, когда стрим состоит из одних чисел, использование оберток над примитивными типами будет сильно влиять на производительность.
Поэтому создатели стримов добавили специальные типы стримов для примитивных типов:
LongStream() DoubleStream() IntStream()
Это такие же стримы, но как понятно из названия оперируют они только одним типом данных.
Также получить стрим из примитивов можно воспользовавшись методами утилитного класса Arrays.stream(). Этот перегруженный метод позволяет обернуть наш массив и получить из него стрим. Стоить отметить, что в нем есть метод static Stream stream(T[] array) , то есть можно получить стрим из массива объектов, а не только примитивных типов.
Теперь мы перейдем к самому интересному — в интерфейсе Collection добавлен дефолтный метод, который возвращает нам стрим. То есть любая коллекция дает нам возможность превратить ее в стрим:
List.of("a", "b", "c").stream().forEach(System.out::println);
Просто вызвав метод у коллекции мы получили стрим. Это самый частый способ получить стрим из набора данных.
Познакомившись с основными методами создания теперь мы можем перейти к промежуточным операциям. Именно они позволят нам обработать наш поток данных.
Промежуточные операции
Мы ранее уже знакомились с операцией фильтр, она позволяет нам написать выражение, которое будет проверятся для каждого элемента и если выражение истинно, то элемент может проходить дальше.
Но на нашем заводе мы делаем намного больше чем просто фильтруем бревна. Для того, чтобы дерево превратилось в мебель его нужно преобразовать.
Для этого пригодится самая популярная функция – map().
Возьмем наш пример выше и попробуем преобразовать
List logs = List.of( new Log("Сибирская сосна", 10), new Log("Дуб монгольский", 30), new Log("Берёза карликовая", 5)); logs.stream().map(x -> x.getType()).forEach(System.out::println);
Функция map принимает реализацию функционального интерфейса Function .
На вход мы получаем объект типа T, а возвращаем объект типа R. То есть наш стрим, который был типизирован объектом Log становится типизирован объектом, который возвращает x.getType() . Мы получаем набор строк с названиями деревьев.
Промежуточные операции можно конкатенировать между собой, то есть мы можем добавить еще несколько преобразований:
logs.stream().map(Log::getType).map(x -> x.split(" ")) .forEach(System.out::println);
Во втором преобразовании мы разбили каждую строку на массив строк. Но если мы запустим приложение, мы увидим, что на экран не вывелись строки, а вывелось toString() массивов. Нам хочется чтобы стрим был плоский — то есть только из объектов, а не из других стримов/массивов в которых есть объекты. Для этого авторы Java Stream API придумали еще одну промежуточную операцию — flatMap. Вот как она позволит изменить нам наш стрим (для более краткой записи я заменил прошлые операции на метод референс):
logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(x -> Arrays.stream(x)).forEach(System.out::println);
На вход flatMap() поступает функция, задача которой получить из объекта стрим и конкатенировать его с другими. Таким образом, мы создаем стримы из массивов строк и соединяем их вместе. Попробуем теперь получить список всех букв, которые встречаются в нашем стриме. Для этого воспользуемся методом chars() . Метод x.chars() класса String возвращает стрим примитивов IntStream. Каждому символу в строке она сопоставляет int значение.
logs.stream().map(Log::getType) .map(x -> x.split(" ")).flatMap(Arrays::stream) .map(String::chars).forEach(System.out::println);
Но запустив пример выше мы получили стрим стримов — Stream
Но так с ним работать не удобно, а обычный flatMap не сработает, поэтому для примитивных стримов существуют специальные операции для их преобразований:
IntStream chars = logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(Arrays::stream).map(String::chars).flatMapToInt(x -> x); chars.forEach(x1 -> System.out.println((char)x1));
Значение функции выше x -> x говорит нам о том, что для того чтобы склеить наши стримы нам не нужно никаких дополнительных преобразований. В терминальной операции forEach мы привели значение x1 к символьной записи.
В итоге предыдущий пример вывел нам на экран побуквенно каждое название типа дерева. Что делать, если мы хотим вывести на экран только по одной букве, убрав повторяющиеся буквы? Для этого мы можем воспользоваться промежуточной операцией distinct() . Сама по себе операция очень похожа на фильтр, только разница в том, что она в себе запоминает все числа, которые через нее прошли и в следующий раз «пропускает» только те объекты, которые еще не прошли.
Для того чтобы отсортировать буквы воспользуемся операцией sorted() :
IntStream chars = logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(Arrays::stream). map(String::chars).flatMapToInt(x -> x).distinct().sorted();
Стоит отметить, что операция sorted() таит в себе некоторые проблемы. Так для того чтобы отсортировать объекты, поступающие из стрима, она должна аккумулировать в себе все объекты, которые есть в стриме и только потом приступить к сортировке. Но что делать, если стрим бесконечный либо в стриме огромное количество элементов? Вызов такой операции приведет к OutOfMemoryException.
Для того, чтобы ограничить бесконечные операции существует операция limit() . В нее в качестве аргумента мы можем передать число элементов, которых мы хотим взять из стрима.
IntStream chars = logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(Arrays::stream). map(String::chars).flatMapToInt(x -> x).distinct().limit(3).sorted()
В примере выше мы с помощью функции limit ограничили наш стрим до трех элементов, которые уже позже попали в sorted() .
Противоположная limit() операция называется skip() . Она принимает в качестве параметра число элементов, которые надо пропустить. То есть если логика нашей программы говорит нам начать обрабатывать элементы только после какого-то.
Порой это не особо удобно, а порой невозможно указать заранее сколько элементов пропустить или поглотить. Поэтому в стримы были добавлены дополнительные промежуточные операции, которые принимают предикат: takeWhile(Predicate predicate) и dropWhile(Predicate predicate) .
Простой пример приведен ниже:
new Random().ints().takeWhile(x -> x % 7 != 0).forEach(System.out::print);
Стрим будет генерировать новые значения, пока остаток от деления на 7 сгенерированного значения не будет равен 0.
И последняя операция, которую хотелось бы описать — это boxed. Ее стоит применять в том случае, если мы хотим превратить наш стрим примитивов в объектный стрим. То есть в примере выше, если добавить ее, то наш стрим перестанет быть IntStream, а станет Stream .
Есть еще несколько промежуточных операций, но я расскажу только об одной, на мой взгляд, самой важной. Это операция parallel() . Разместив ее в любом месте нашего стрима мы волшебным образом запускаем очень сложный механизм внутри JVM. Мы получаем возможность многопоточной обработки нашего стрима. То есть Stream API постарается максимально эффективно выполнить все операции на разных ядрах процессора.
Терминальные операции
После знакомства с основными промежуточными операциями мы плавно подошли к заключению. Осталось рассмотреть терминальные операции. Это операции, которые как бы «запускают» наш стрим. Мы можем создать стрим и добавить в него любое количество промежуточных операций, но они не будут выполнены пока не будут добавлена терминальная операция.
Выше мы уже применяли одну из самых популярных операций — forEach(Consumer action) . В нее попадают все прошедшие через стрим объекты и обрабатываются в соответствие с тем алгоритмом, что будет указан в Consumer.
Кроме этого терминальная операция может и возвращать значение. Рассмотрим самые распространенные — findFirst(), findAny(), anyMatch(), allMatch(), noneMatch().
Функции findFirst() и findAny() возвращают единственное значение, если оно есть, обернутое в Optional. Как нетрудно догадаться, в первом случае мы получим первый элемент нашего стрима, а во втором произвольный элемент из него, при условии, конечно, что элемент существует, иначе вернется Optional.empty() .
Функции anyMatch(Predicate predicate), allMatch(Predicate predicate), noneMatch(Predicate predicate) позволяют проверить элементы стрима на определенное условие и вернуть true или false. Первая функция пробегается по элементам стрима до тех пор пока хотя бы для одного элемента не будет выполнено условие, если таких элементов нет, то возвращается false. Противоположным образом работает allMatch() - true возвращается только в том случае, если все элементы подходят, если хотя бы для одного элемента предикат вернул false, то терминальная операция сразу же вернет тоже самое. noneMath представляет из себя тоже самое, что и allMatch , только с инвертированной функцией предиката.
Теперь стоит перейти к более сложным функциям. Часто в качестве результата стрима мы хотим получить набор из новых объектов, которые были созданы в результате обработки. Для этого удобно поместить их в массив или коллекцию.
В Java Stream API было добавлено несколько методов, которые дают соответствующую функциональность.
Вызвав терминальную операцию Object[] toArray() мы получим ссылку на массив, в котором будет находится все объекты. Если нужно вернуть массив определенного типа, то в метод стоить передать IntFunction generator на вход функции поступит число элементов, а внутри нее мы должны создать нужный нам тип массива.
Следующая операция, которую стоить упомянуть T reduce(T identity, BinaryOperator accumulator) — в нее передается начальное значение и бинарная функция, которая задает алгоритм объединения двух объектов.
Для того, чтобы получить сумму первых 100 членов стрима из произвольных значений, запишем:
new Random().ints(100).reduce(0, (x, y) -> x + y);
Мы передаем начальный элемент для сложения, в нашем случае он 0 и бинарную функцию, которая описывает как объединить два значения из стрима.
Если же мы хотим перенести этот набора чисел в коллекцию, то для этого нам надо будет указать как создать коллекцию и как в нее помещать элементы:
List ints = new Random().ints(100) .boxed() // оборачиваем, так как коллекции не работают с примитивами .reduce(new ArrayList<>(), (x, y) -> < x.add(y); return x; >, (a, b) -> < a.addAll(b); return a >);
В функции reduce мы передали наш начальный аргумент — новую пустую коллекцию. Потом описали правило, по которому будем объединять коллекцию и элементы стрима.
И в конце описали как мы будем объединять две коллекции.
Чтобы сократить подобную запись, была создана терминальная операция collect(Collector collector) .Наше выражение выше перепишется так:
List ints = new Random().ints(100) .boxed() .collect(Collectors.toList());
То есть вся логика комбинирования элементов хранится в структуре данных под названием коллектор.
Создатели Java Stream API добавили в библиотеку большое количество коллекторов, рассмотрим их.
Выше мы уже познакомились с коллектором, который комбинирует элементы в список. Если нам нужно собрать элементы в коллекцию типа Set, то достаточно просто использовать коллектор Collectors.toSet() .
Существует более общий метод Collectors.toCollection(). В качестве аргумента в нее можно передать коллекцию, в которую будут помещены элементы стрима.
Более сложный коллектор — toMap() . Коллектору надо объяснить как собирать словарь: что нужно делать с объектом, чтобы получить ключ и значение, а так же как себя вести в случае если ключи совпадают.
Подсчитаем количество букв, которые мы получили в стриме chars() .
chars.collect(Collectors.toMap(x -> x, x -> 1, Integer::sum))
Для ключа мы используем саму букву без изменений x->x . Далее каждой новой букве сопоставляем число 1. Если буквы совпали, то складываем для них числа в значении values.
Операция partitionBy() позволяет разделить стрим на два множества по определенному условию. Например мы хотим разделить наш буквенный стрим на две группы с большими буквами и прописными:
chars.collect(Collectors.partitioningBy(Character::isLowerCase))
Коллекторы могут быть скомбинированы друг с другом, что дает большую гибкость.
В примере выше мы видим, что некоторые буквы повторяются, мы этого не хотим поэтому добавим еще один коллектор, который соберет все в Set:
chars.collect(Collectors.partitioningBy(Character::isLowerCase, Collectors.toSet()))
Метод groupingBy() выполняет похожую работу, что и toMap() c той лишь разницей, что в него можно дополнительно передать цепочку коллекторов, как и в случае с partitioningBy() .
Чтобы самостоятельно реализовать коллектор можно воспользоваться статическим методом:
Collector of(Supplier supplier, BiConsumer accumulator, BinaryOperator combiner, Characteristics. characteristics)
Очень похоже на метод reduce() , который мы рассматривали ранее с той лишь, разницей, что нужно добавить в стрим характеристики коллектора. Они указывают на свойства коллектора, которые могут быть использованы для оптимизации. Например, может ли быть коллектор параллельным, что позволит существенно ускорить комбинирование.
В этой короткой статье мы познакомились с, на мой взгляд, самой крутой штукой в языке Java с момента ее создания. Стримы позволяют существенно упростить, а соответственно ускорить разработку кода. Возможность практически бесплатно сделать стрим параллельным, тем самым повысив производительность кода в разы, делает стримы инструментом номер одни в руках каждого разработчика.