Кофе-брейк #177. Подробное руководство по Java Stream в Java 8

Статья из группы Random
Источник: Hackernoon В этой публикации изложено подробное руководство по работе с Java Stream вместе с примерами кода и пояснениями. Кофе-брейк #177. Подробное руководство по Java Stream в Java 8 - 1

Введение в потоки Java в Java 8

Потоки Java (Streams), представленные как часть Java 8, используются для работы с коллекциями данных. Сами по себе они не являются структурой данных, но могут использоваться для ввода информации из других структур данных путем упорядочивания и конвейерной обработки для получения окончательного результата. Примечание: Важно не путать Stream и Thread, поскольку в русском языке оба термина часто упоминаются в одинаковом переводе “поток”. Stream обозначает объект для выполнения операций (чаще всего передача данных или их накопление), тогда как Thread (дословный перевод — нить) обозначает объект, который позволяет выполнить определенный программный код параллельно с другими ветками кода. Поскольку Stream — это не отдельная структура данных, она никогда не изменяет источник данных. Потоки Java имеют следующие особенности:
  1. Java Stream можно использовать с помощью пакета “java.util.stream”. Его можно импортировать в скрипт с помощью кода:

    
    import java.util.stream.* ;
    

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

  2. Java Stream может принимать входные данные из коллекций данных, таких как коллекции и массивы в Java.

  3. Java Stream не требует изменения структуры входных данных.

  4. Java Stream не изменяет источник. Вместо этого он генерирует выходные данные с помощью соответствующих конвейерных методов.

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

  6. В Java Stream промежуточные операции конвейеризированы и происходят в формате отложенных или “ленивых” вычислений (lazy evaluation). Они завершаются терминальными функциями. Это формирует базовый формат использования Java Stream.

В следующем разделе мы рассмотрим различные способы, используемые в Java 8 для создания Java Stream по мере необходимости.

Создание Java Stream в Java 8

Потоки Java можно создавать несколькими способами:

1. Создание пустого потока с помощью метода Stream.empty()

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

Stream<String> str = Stream.empty();
Приведенный выше оператор сгенерирует пустой поток с именем str без каких-либо элементов внутри него. Чтобы убедиться в этом достаточно проверить количество или размер потока с помощью термина str.count(). Например,

System.out.println(str.count());
В результате получим на выводе 0.

2. Создание потока с использованием метода Stream.builder() с экземпляром Stream.Builder

Мы также можем использовать Stream Builder для создания потока с помощью шаблона проектирования builder. Он предназначен для пошагового построения объектов. Давайте посмотрим, как мы можем создать экземпляр потока с помощью Stream Builder.

Stream.Builder<Integer> numBuilder = Stream.builder();

numBuilder.add(1).add(2).add( 3);

Stream<Integer> numStream = numBuilder.build();
Используя этот код, можно создать поток с именем numStream, содержащий элементы int. Все выполняется достаточно быстро благодаря экземпляру Stream.Builder под именем numBuilder, который создается первым.

3. Создание потока с указанными значениями с помощью метода Stream.of()

Еще один способ создания потока предполагает использование метода Stream.of(). Это простой способ создания потока с заданными значениями. Он объявляет, а также инициализирует поток. Пример использования метода Stream.of() для создания потока:

Stream<Integer> numStream = Stream.of(1, 2, 3);
Этот код создаст поток, содержащий элементы int, как мы уже сделали в предыдущем методе с использованием Stream.Builder. Здесь мы напрямую создали поток, используя Stream.of() с заранее заданными значениями [1, 2 и 3].

4. Создание потока из существующего массива с использованием метода Arrays.stream()

Другой распространенный метод создания потока предполагает использование массивов в Java. Поток здесь создается из существующего массива с помощью метода Arrays.stream(). Все элементы массива преобразуются в элементы потока. Вот наглядный пример:

Integer[] arr = {1, 2, 3, 4, 5};

Stream<Integer> numStream = Arrays.stream(arr);
Этот код будет генерировать поток numStream, содержащий содержимое массива с именем arr, который представляет собой целочисленный массив (integer array).

5. Объединение двух существующих потоков с помощью метода Stream.concat()

Еще один метод, который можно использовать для создания потока, — это метод Stream.concat(). Он используется для объединения двух потоков с целью создания единого потока. Оба потока объединяются по порядку. Это означает, что первым идет первый поток, за которым следует второй поток, и так далее. Пример такой конкатенации выглядит следующим образом:

Stream<Integer> numStream1 = Stream.of(1, 2, 3, 4, 5);

Stream<Integer> numStream2 = Stream.of(1, 2, 3);

Stream<Integer> combinedStream = Stream.concat( numStream1, numStream2);
Приведенный выше оператор создаст окончательный поток с именем combinedStream, содержащий один за другим элементы первого потока numStream1 и второго потока numStream2.

Типы операций с Java Stream

Как уже упоминалось, с Java Stream в Java 8 можно выполнять два типа операций: промежуточные (intermediate) и терминальные (terminal). Рассмотрим каждую из них более подробно.

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

Промежуточные операции генерируют выходной поток (output stream) и выполняются только при встрече с терминальной операцией. Это означает, что промежуточные операции выполняются “лениво”, конвейерно и могут быть завершены только терминальной операцией. О ленивом вычислении и конвейерной обработке вы узнаете чуть позже. Примерами промежуточных операций являются следующие методы: filter(), map(), different(), peek(), sorted() и некоторые другие.

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

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

Примеры операций с Java Stream

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

Вот несколько примеров некоторых промежуточных операций, которые можно применять к Java Stream:

filter()

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

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> even = numStream.filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println(even);
Вывод:
[98]
Объяснение: В этом примере видно, что четные элементы (делящиеся на 2) фильтруются с помощью метода filter() и сохраняются в целочисленном списке numStream, содержимое которого печатается позже. Поскольку 98 — единственное четное целое число в потоке, оно печатается в выводе.

map()

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

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> d = numStream.map(n -> n*2) .collect(Collectors.toList()); System.out.println(d);
Вывод:
[86, 130, 2, 196, 126]
Объяснение: Здесь мы видим, что метод map() используется для простого удвоения каждого элемента потока numStream. Как видно из вывода, каждый из элементов в потоке успешно удвоен.

distinct()

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

Stream<Integer> numStream = Stream.of(43,65,1,98,63,63,1); List<Integer> numList = numStream.distinct() .collect(Collectors.toList()); System.out.println(numList);
Вывод:
[43, 65, 1, 98, 63]
Объяснение: В этом случае для numStream используется метод different(). Он извлекает все отдельные элементы в списке numList путем удаления дубликатов из потока. Как видно из выводных данных, дубликатов нет, в отличие от входного потока, который изначально имел два дубликата (63 и 1).

peek()

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

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> nList = numStream.map(n -> n*10) .peek(n->System.out.println("Mapped: "+ n)) .collect(Collectors.toList()); System.out.println(nList);
Вывод:
Mapped: 430 Mapped: 650 Mapped: 10 Mapped: 980 Mapped: 630 [430, 650, 10, 980, 630]
Объяснение: Здесь метод peek() используется для генерации промежуточных результатов, поскольку метод map() применяется к элементам потока. Здесь мы можем заметить, что даже до применения терминальной операции collect() для печати окончательного содержимого списка в операторе print результат для каждого сопоставления элемента потока печатается последовательно заранее.

sorted()

Метод sorted() используется для сортировки элементов потока. По умолчанию он сортирует элементы в порядке возрастания. Также можно указать конкретный порядок сортировки в качестве параметра. Пример реализации этого метода выглядит следующим образом:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.sorted().forEach(n -> System.out.println(n));
Вывод:
1 43 ​​63 65 98
Объяснение: Здесь метод sorted() используется для сортировки элементов потока в порядке возрастания по умолчанию (поскольку конкретный порядок не указан). Можно увидеть, что элементы, напечатанные в выводе, упорядочены в возрастающем порядке.

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

Вот несколько примеров некоторых терминальных операций, которые можно применять к потокам Java:

forEach()

Метод forEach() используется для перебора всех элементов потока и выполнения функции для каждого элемента один за другим. Это действует как альтернатива операторам цикла, таким как for, while и другим. Пример:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.forEach(n -> System.out.println(n));
Вывод:
43 65 1 98 63
Объяснение: Здесь метод forEach() используется для печати каждого элемента потока один за другим.

count()

Метод count() используется для извлечения общего количества элементов, присутствующих в потоке. Он похож на метод size(), который часто используется для определения общего количества элементов в коллекции. Пример использования метода count() с Java Stream выглядит следующим образом:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); System.out.println(numStream.count());
Вывод:
5
Объяснение: Поскольку поток numStream содержит 5 целочисленных элементов, то при использовании для него метода count() вывод будет 5.

collect()

Метод collect() используется для выполнения изменяемых сокращений элементов потока. Его можно использовать для удаления содержимого из потока после завершения обработки. Для проведения редукций он использует класс Collector.

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> odd = numStream.filter(n -> n % 2 == 1) .collect(Collectors.toList()); System.out.println(odd);
Вывод:
[43, 65, 1, 63]
Объяснение: В этом примере все нечетные элементы в потоке фильтруются и собираются/сокращаются в список с именем odd (нечетные). В конце печатается список нечетных.

min() и max()

Метод min(), как следует из названия, может использоваться в потоке для поиска в нем минимального элемента. Точно так же метод max() можно использовать для поиска максимального элемента в потоке. Давайте попробуем понять, как их можно использовать, на примере:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); int smallest = numStream.min((m, n) -> Integer.compare(m, n)).get(); System.out.println("Smallest element: " + smallest);

numStream = Stream.of(43, 65, 1, 98, 63); int largest = numStream.max((m, n) -> Integer.compare(m, n)).get(); System.out.println("Largest element: " + largest);
Вывод:
Smallest element: 1 Largest element: 98
Объяснение: В этом примере мы напечатали наименьший элемент в потоке numStream с помощью метода min() и самый большой элемент с помощью метода max(). Обратите внимание, что здесь перед применением метода max() мы снова добавили элементы в поток numStream. Это связано с тем, что min() является терминальной операцией и уничтожает содержимое исходного потока, возвращая только окончательный результат (который в данном случае был целочисленным “smallest”).

findAny() и findFirst()

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

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); Optional<Integer> opt = numStream.findFirst();System.out.println(opt); numStream = Stream.empty(); opt = numStream.findAny();System.out.println(opt);
Вывод:
Optional[43] Optional.empty
Объяснение: Здесь, в первом случае, метод findFirst() возвращает первый элемент потока как Optional. Затем, когда поток переназначается как пустой поток, метод findAny() возвращает пустой Optional.

allMatch(), anyMatch() и noneMatch()

Метод allMatch() используется для проверки того, соответствуют ли все элементы в потоке определенному предикату, и возвращает логическое значение true, если это так, в противном случае возвращается false. Если поток пуст, он возвращает true. Метод anyMatch() используется для проверки того, соответствует ли какой-либо из элементов в потоке определенному предикату. Он возвращает true, если это так, и false в противном случае. Если поток пуст, он возвращает false. Метод noneMatch() возвращает true, если ни один элемент потока не соответствует предикату, и false в противном случае. Пример, иллюстрирующий это, выглядит следующим образом:

Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); boolean flag = numStream.allMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.anyMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.noneMatch(n -> n==1);System.out.println(flag);
Вывод:
false true false
Объяснение: Для потока numStream, содержащего 1 в качестве элемента, метод allMatch() возвращает false, так как все элементы не равны 1, а только один из них. Метод anyMatch() возвращает true, поскольку хотя бы один из элементов равен 1. Метод noneMatch() возвращает false, поскольку 1 действительно существует как элемент в этом потоке.

Lazy Evaluations (ленивые вычисления) в Java Stream

Ленивые вычисления приводят к оптимизации при работе с Java Streams в Java 8. Они в основном связаны с задержкой выполнения промежуточных операций до тех пор, пока не встретится терминальная операция. Ленивые вычисления отвечают за предотвращение ненужной траты ресурсов на вычисления до тех пор, пока результат действительно не понадобится. Выходной поток, полученный в результате промежуточных операций, генерируется только после выполнения терминальной операции. Ленивые вычисления работают со всеми промежуточными операциями в потоках Java. Очень полезное применение ленивых вычислений проявляется при работе с бесконечными потоками (infinite streams). В этом случае предотвращается много ненужной обработки.

Конвейеры в Java Stream

Конвейер (pipeline) в Java Stream включает в себя входной поток, ноль или несколько промежуточных операций, выстроенных одна за другой, и, наконец, терминальную операцию. Промежуточные операции в Java Streams выполняются “лениво”. Это делает конвейерные промежуточные операции неизбежными. С помощью конвейеров, которые в основном представляют собой промежуточные операции, объединенные по порядку, ленивое выполнение становится возможным. Конвейеры помогают отслеживать промежуточные операции, которые необходимо выполнить после того, как наконец встретится терминальная операция.

Заключение

Давайте теперь подведем итог тому, что мы сегодня изучили. В этой статье:
  1. Мы кратко рассмотрели, что такое потоки Java (Java Stream).
  2. Затем мы узнали множество различных методов создания потоков Java в Java 8.
  3. Мы изучили два основных типа операций (промежуточные операции и терминальные операции), которые можно выполнять с потоками Java.
  4. Затем мы подробно рассмотрели несколько примеров как промежуточных, так и терминальных операций.
  5. В итоге мы более подробно узнали о ленивых вычислениях и, наконец, изучили конвейерную обработку в потоках Java.
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Константин Уровень 1
29 ноября 2022
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); boolean flag = numStream.allMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.anyMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.noneMatch(n -> n==1);System.out.println(flag); И вот тут два раза. n -> n1
Serge Уровень 16
18 ноября 2022
Очепятка в объяснении метода distinct, написано используется метод different.