
Введение в потоки Java в Java 8
Потоки Java (Streams), представленные как часть Java 8, используются для работы с коллекциями данных. Сами по себе они не являются структурой данных, но могут использоваться для ввода информации из других структур данных путем упорядочивания и конвейерной обработки для получения окончательного результата. Примечание: Важно не путать Stream и Thread, поскольку в русском языке оба термина часто упоминаются в одинаковом переводе “поток”. Stream обозначает объект для выполнения операций (чаще всего передача данных или их накопление), тогда как Thread (дословный перевод — нить) обозначает объект, который позволяет выполнить определенный программный код параллельно с другими ветками кода. Поскольку Stream — это не отдельная структура данных, она никогда не изменяет источник данных. Потоки Java имеют следующие особенности:Java Stream можно использовать с помощью пакета “java.util.stream”. Его можно импортировать в скрипт с помощью кода:
import java.util.stream.* ;
Используя этот код, мы также можем легко реализовать несколько встроенных функций в Java Stream.
Java Stream может принимать входные данные из коллекций данных, таких как коллекции и массивы в Java.
Java Stream не требует изменения структуры входных данных.
Java Stream не изменяет источник. Вместо этого он генерирует выходные данные с помощью соответствующих конвейерных методов.
Java Stream подвергаются промежуточным и терминальным операциям, которые мы обсудим в следующих разделах.
В Java Stream промежуточные операции конвейеризированы и происходят в формате отложенных или “ленивых” вычислений (lazy evaluation). Они завершаются терминальными функциями. Это формирует базовый формат использования 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);
Вывод:
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);
Вывод:
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);
Вывод:
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);
Вывод:
sorted()
Метод sorted() используется для сортировки элементов потока. По умолчанию он сортирует элементы в порядке возрастания. Также можно указать конкретный порядок сортировки в качестве параметра. Пример реализации этого метода выглядит следующим образом:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.sorted().forEach(n -> System.out.println(n));
Вывод:
Терминальные операции
Вот несколько примеров некоторых терминальных операций, которые можно применять к потокам Java:forEach()
Метод forEach() используется для перебора всех элементов потока и выполнения функции для каждого элемента один за другим. Это действует как альтернатива операторам цикла, таким как for, while и другим. Пример:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.forEach(n -> System.out.println(n));
Вывод:
count()
Метод count() используется для извлечения общего количества элементов, присутствующих в потоке. Он похож на метод size(), который часто используется для определения общего количества элементов в коллекции. Пример использования метода count() с Java Stream выглядит следующим образом:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); System.out.println(numStream.count());
Вывод:
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);
Вывод:
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);
Вывод:
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);
Вывод:
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);
Вывод:
Lazy Evaluations (ленивые вычисления) в Java Stream
Ленивые вычисления приводят к оптимизации при работе с Java Streams в Java 8. Они в основном связаны с задержкой выполнения промежуточных операций до тех пор, пока не встретится терминальная операция. Ленивые вычисления отвечают за предотвращение ненужной траты ресурсов на вычисления до тех пор, пока результат действительно не понадобится. Выходной поток, полученный в результате промежуточных операций, генерируется только после выполнения терминальной операции. Ленивые вычисления работают со всеми промежуточными операциями в потоках Java. Очень полезное применение ленивых вычислений проявляется при работе с бесконечными потоками (infinite streams). В этом случае предотвращается много ненужной обработки.Конвейеры в Java Stream
Конвейер (pipeline) в Java Stream включает в себя входной поток, ноль или несколько промежуточных операций, выстроенных одна за другой, и, наконец, терминальную операцию. Промежуточные операции в Java Streams выполняются “лениво”. Это делает конвейерные промежуточные операции неизбежными. С помощью конвейеров, которые в основном представляют собой промежуточные операции, объединенные по порядку, ленивое выполнение становится возможным. Конвейеры помогают отслеживать промежуточные операции, которые необходимо выполнить после того, как наконец встретится терминальная операция.Заключение
Давайте теперь подведем итог тому, что мы сегодня изучили. В этой статье:- Мы кратко рассмотрели, что такое потоки Java (Java Stream).
- Затем мы узнали множество различных методов создания потоков Java в Java 8.
- Мы изучили два основных типа операций (промежуточные операции и терминальные операции), которые можно выполнять с потоками Java.
- Затем мы подробно рассмотрели несколько примеров как промежуточных, так и терминальных операций.
- В итоге мы более подробно узнали о ленивых вычислениях и, наконец, изучили конвейерную обработку в потоках Java.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ