Java 8 Stream collect to collections

Stream API — новый способ взаимодействия с данными, представляя их в виде конечного потока данных.
Один из методов Stream-ов, collect принимает лямбда-выражение известное как коллектор (Collector), которое собирает данные в необходимую структуру данных.
С ними мы сегодня и научимся работать.
2. Collect to List + filter
Отфильтруем список чисел по четности
До Java 8
public static void main ( String [ ] args ) <
List
List
for ( Integer i : numbers ) <
if ( i % 2 == 0 ) <
evenNumbers . add ( i ) ;
System . out . println ( evenNumbers ) ; //output [2, 4, 6, 8, 10]
Теперь мы можем использовать методы filter и collect
public static void main ( String [ ] args ) <
List
List
. filter ( i — > i % 2 == 0 )
. collect ( Collectors . toList ( ) ) ;
System . out . println ( evenNumbers ) ; //output [2, 4, 6, 8, 10]
К счастью, свой коллектор нам писать не нужно т.к. в Java 8 добавили утилитный класс Collectors с необходимыми методами. Collect в сочетании с Collectors.toList возвращают List (реализация — ArrayList)
3. Collect to Set + map
Преобразуем список имен в набор(Set) имен в верхнем регистре
До Java 8
public static void main ( String [ ] args ) <
List
Set
for ( String name : names ) <
upperCaseNames . add ( name . toUpperCase ( ) ) ;
System . out . println ( upperCaseNames ) ; //output [ARYA, JOHN, SANSA]
Сейчас же мы можем воспользоваться методом map со ссылкой на метод, а так же утилитным классом Collectors
public static void main ( String [ ] args ) <
List
Set
. map ( String : : toUpperCase )
. collect ( Collectors . toSet ( ) ) ;
System . out . println ( upperCaseNames ) ; //output [ARYA, JOHN, SANSA]
Collect в сочетании с Collectors.toSet возвращают Set (реализация — HashSet)
4. Collect to Collection
Соберем список не пустых строк в Queue
До Java 8
public static void main ( String [ ] args ) <
List
Queue
for ( String name : names ) <
if ( ! name . isEmpty ( ) ) <
queue . add ( name ) ;
System . out . println ( queue ) ; //output [Jaime, Daenerys, Tyrion]
Чтобы проделать то же самое с помощью Stream API необходимо воспользоваться другим методом класса Collections — toCollection
public static void main ( String [ ] args ) <
List
Queue
. filter ( n — > ! n . isEmpty ( ) )
. collect ( Collectors . toCollection ( ( ) — > new LinkedList < >( ) ) ) ;
System . out . println ( queue ) ; //output [Jaime, Daenerys, Tyrion]
Метод toCollection принимает лямбда-выражение типа поставщик (Supplier), которое должно вернуть коллекцию, в которую мы хотим сохранить данные. В данном случае мы вернули LinkedList.
Пример так же можно улучшить с помощю ссылки на метод, изменив
Collectors . toCollection ( ( ) — > new LinkedList < >( ) )
Collectors . toCollection ( LinkedList : : new )
public static void main ( String [ ] args ) <
List
Queue
. filter ( n — > ! n . isEmpty ( ) )
. collect ( Collectors . toCollection ( LinkedList : : new ) ) ;
System . out . println ( queue ) ; //output [Jaime, Daenerys, Tyrion]
Надеемся, что наша статья была Вам полезна. Также есть возможность записаться на наши курсы по Java в Киеве. Детальную информацию Вы можете найти у нас на сайте.
- ← Java 8 Stream map()
- Java 8 Stream flatMap() →
Вам также может понравиться.
Java 8 CompletableFuture. Часть 3 — Конец
28 октября, 2017 Vertex Academy Комментарии к записи Java 8 CompletableFuture. Часть 3 — Конец отключены
Класс коллекторов – методы toList(), toSet() и toMap()
В этом посте будет обсуждаться toList() , toSet() , а также toMap() методы Collectors класс на Java.
1. Использование Collectors.toList() метод
Collectors.toList() метод возвращает Collector который собирает все входные элементы в новый список. Он не дает никаких гарантий относительно типа возвращаемого списка.
⮚ Получить список из указанных значений
List
. collect ( Collectors . toList ( ) ) ;
⮚ Преобразование примитивного массива в список
int [ ] array = < 1 , 2 , 3 , 4 , 5 >;
List
. flatMapToInt ( Arrays :: stream )
. collect ( Collectors . toList ( ) ) ;
⮚ Преобразование непримитивного массива в список
Integer [ ] arr = < 1 , 2 , 3 , 4 , 5 >;
List
. collect ( Collectors . toList ( ) ) ;
⮚ Преобразование строки в список символов
List
. mapToObj ( e -> ( char ) e ) // Stream
. collect ( Collectors . toList ( ) ) ;
2. Использование Collectors.toSet() метод
Collectors.toSet() метод работает так же, как Collectors.toList() но возвращает набор, который отбрасывает дубликаты, присутствующие во входных данных.
⮚ Получить набор из указанных значений
Set
⮚ Преобразование примитивного массива в набор
int [ ] array = < 1 , 2 , 3 , 4 , 5 >;
Set
. flatMapToInt ( Arrays :: stream )
. collect ( Collectors . toSet ( ) ) ;
⮚ Преобразование непримитивного массива в набор
Integer [ ] arr = < 1 , 2 , 3 , 4 , 5 >;
Set
3. Использование Collectors.toMap() метод
Поскольку поток представляет собой последовательность элементов, для создания из него карты нам нужно указать, как извлекать пары ключ/значение. Collectors.toMap() возвращает Collector который собирает все входные элементы в новую карту, применяя указанные функции отображения к входным элементам, которые помогают идентифицировать ключи и значения.
Map < String , String >map = Stream . of ( new String [ ] [ ] < > ) . collect ( Collectors . toMap ( p -> p [ 0 ] , p -> p [ 1 ] ) ) ;
Если нам нужна карта с разными типами ключа и значения, мы можем сделать что-то вроде:
Map < String , Integer >map = Stream . of ( new Object [ ] [ ] < > ) . collect ( Collectors . toMap ( p -> ( String ) p [ 0 ] , p -> ( Integer ) p [ 1 ] ) ) ;
Другой подход к использованию различных типов ключей и значений заключается в использовании AbstractMap.SimpleEntry или же AbstractMap.SimpleImmutableEntry , которые являются просто реализациями Map.Entry интерфейс.
Collectors tolist что возвращает
Большинство операций класса Stream, которые модифицируют набор данных, возвращают этот набор в виде потока. Однако бывают ситуации, когда хотелось бы получить данные не в виде потока, а в виде обычной коллекции, например, ArrayList или HashSet. И для этого у класса Stream определен метод collect . Первая версия метода принимает в качестве параметра функцию преобразования к коллекции:
R collect(Collector collector)
Параметр R представляет тип результата метода, параметр Т — тип элемента в потоке, а параметр А — тип промежуточных накапливаемых данных. В итоге параметр collector представляет функцию преобразования потока в коллекцию.
Эта функция представляет объект Collector, который определен в пакете java.util.stream. Мы можем написать свою реализацию функции, однако Java уже предоставляет ряд встроенных функций, определенных в классе Collectors :
- toList() : преобразование к типу List
- toSet() : преобразование к типу Set
- toMap() : преобразование к типу Map
Например, преобразуем набор в потоке в список:
import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class Program < public static void main(String[] args) < Listphones = new ArrayList(); Collections.addAll(phones, "iPhone 8", "HTC U12", "Huawei Nexus 6P", "Samsung Galaxy S9", "LG G6", "Xiaomi MI6", "ASUS Zenfone 2", "Sony Xperia Z5", "Meizu Pro 6", "Lenovo S850"); List filteredPhones = phones.stream() .filter(s->s.length() <10) .collect(Collectors.toList()); for(String s : filteredPhones)< System.out.println(s); >> >
Использование метода toSet() аналогично.
Set filteredPhones = phones.stream() .filter(s->s.length()<10) .collect(Collectors.toSet());
Для применения метода toMap() надо задать ключ и значение. Например, пусть у нас есть следующая модель:
class Phone < private String name; private int price; public Phone(String name, int price)< this.name=name; this.price=price; >public String getName() < return name; >public int getPrice() < return price; >>
Теперь применим метод toMap() :
import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; public class Program < public static void main(String[] args) < StreamphoneStream = Stream.of(new Phone("iPhone 8", 54000), new Phone("Nokia 9", 45000), new Phone("Samsung Galaxy S9", 40000), new Phone("LG G6", 32000)); Map phones = phoneStream .collect(Collectors.toMap(p->p.getName(), t->t.getPrice())); phones.forEach((k,v)->System.out.println(k + " " + v)); > > class Phone < private String name; private int price; public Phone(String name, int price)< this.name=name; this.price=price; >public String getName() < return name; >public int getPrice() < return price; >>
Лямбда-выражение p->p.getName() получает значение для ключа элемента, а t->t.getPrice() - извлекает значение элемента.
Если нам надо создать какой-то определенный тип коллекции, например, HashSet, то мы можем использовать специальные функции, которые определены в классах-коллекций. Например, получим объект HashSet:
import java.util.HashSet; import java.util.stream.Collectors; import java.util.stream.Stream; public class Program < public static void main(String[] args) < Streamphones = Stream.of("iPhone 8", "HTC U12", "Huawei Nexus 6P", "Samsung Galaxy S9", "LG G6", "Xiaomi MI6", "ASUS Zenfone 2", "Sony Xperia Z5", "Meizu Pro 6", "Lenovo S850"); HashSet filteredPhones = phones.filter(s->s.length()<12). collect(Collectors.toCollection(HashSet::new)); filteredPhones.forEach(s->System.out.println(s)); > >
Выражение HashSet::new представляет функцию создания коллекции. Аналогичным образом можно получать другие коллекции, например, ArrayList:
ArrayList result = phones.collect(Collectors.toCollection(ArrayList::new));
Вторая форма метода collect имеет три параметра:
R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
- supplier : создает объект коллекции
- accumulator : добавляет элемент в коллекцию
- combiner : бинарная функция, которая объединяет два объекта
Применим эту версию метода collect:
import java.util.ArrayList; import java.util.stream.Collectors; import java.util.stream.Stream; public class Program < public static void main(String[] args) < Streamphones = Stream.of("iPhone 8", "HTC U12", "Huawei Nexus 6P", "Samsung Galaxy S9", "LG G6", "Xiaomi MI6", "ASUS Zenfone 2", "Sony Xperia Z5", "Meizu Pro 6", "Lenovo S850"); ArrayList filteredPhones = phones.filter(s->s.length()<12) .collect( ()->new ArrayList(), // создаем ArrayList (list, item)->list.add(item), // добавляем в список элемент (list1, list2)-> list1.addAll(list2)); // добавляем в список другой список filteredPhones.forEach(s->System.out.println(s)); > >
Глубокое погружение в 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, и безусловно, этот инструмент заслуживает времени, уделенного на его изучение.