Java Stream API: реальные примеры для начинающих

Поток в Java можно определить как последовательность элементов из источника. Источник элементов здесь относится к коллекции или массиву, которые предоставляют данные потоку.

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

1. Что такое поток?

Все мы смотрели онлайн-видео на YouTube. Когда мы начинаем смотреть видео, небольшая часть видеофайла сначала загружается на наш компьютер и начинает воспроизводиться. Нам не нужно загружать все видео, прежде чем начать его смотреть. Это называется потоковым видео. На очень высоком уровне мы можем рассматривать небольшие части видеофайла как поток, а все видео как коллекцию.

На гранулярном уровне разница между Коллекцией и Потоком заключается в том, когда вычисляются данные. Коллекция — это структура данных в памяти, которая содержит все значения структуры данных. Каждый элемент в Коллекции должен быть вычислен, прежде чем его можно будет добавить в Коллекцию. В то время как Поток концептуально представляет собой конвейер, в котором элементы вычисляются по требованию.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// Using a Stream to filter even numbers and then double themList<Integer> evenNumber = numbers.stream().filter(n -> n % 2 == 0) // Filter even numbers.toList(); // Collect the results into a new listSystem.out.println("Even Numbers List: " + evenNumber); // [2, 4, 6, 8, 10]

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

В Java интерфейс java.util.Stream представляет собой поток, над которым можно выполнить одну или несколько операций.

  • Потоковые операции бывают промежуточными или конечными. Конечные операции возвращают результат определенного типа, а промежуточные операции возвращают сам поток, поэтому мы можем сцепить несколько методов подряд, чтобы выполнить операцию в несколько шагов.
  • Потоки создаются на основе источника, например, java.util.Collection, например List или Set. Карта не поддерживается напрямую, мы можем создать поток ключей карты, значений или записей.
  • Потоковые операции могут выполняться последовательно или параллельно. При параллельном выполнении они называются параллельным потоком.

На основании вышеизложенного можно сказать, что поток — это:

  • Разработано для лямбда-выражений или функционального программирования.
  • Не является структурой данных для хранения объектов.
  • Не поддерживают индексированный доступ
  • Можно легко объединить в массивы или списки.
  • Поддерживается ленивый доступ
  • Параллелизуемый

2. Создание потоков

Приведенные ниже способы являются наиболее популярными способами создания потоков из коллекций.

2.1.Stream.of()

В данном примере мы создаем поток из фиксированного количества целых чисел.

Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9);stream.forEach(p -> System.out.println(p));

2.2. Поток.из(массива)

В данном примере мы создаем поток из массива. Элементы в потоке берутся из массива.

Stream<Integer> stream = Stream.of( new Integer[]{1,2,3,4,5,6,7,8,9} );stream.forEach(p -> System.out.println(p));

2.3.Список.stream()

В данном примере мы создаем поток из Списка. Элементы в потоке берутся из Списка.

List<Integer> list = new ArrayList<Integer>();for(int i = 1; i< 10; i++){list.add(i);}Stream<Integer> stream = list.stream();stream.forEach(p -> System.out.println(p));

2.4. Stream.generate() или Stream.iterate()

В данном примере мы создаем поток из сгенерированных элементов. Это даст поток из 20 случайных чисел. Мы ограничили количество элементов с помощью функции limit().

Stream<Integer> randomNumbers = Stream.generate(() ->(new Random()).nextInt(100));randomNumbers.limit(20).forEach(System.out::println);

2.5 Поток строковых символов или токенов

В данном примере сначала создаем поток из символов заданной строки, а во второй части создаем поток токенов, полученных в результате разбиения строки.

IntStream stream = "12345_abcdefg".chars();stream.forEach(p -> System.out.println(p));//ORStream<String> stream = Stream.of("A$B$C".split("\\$"));stream.forEach(p -> System.out.println(p));

Есть еще несколько способов, например, использование Stream.Buider или использование промежуточных операций. Мы будем изучать их в отдельных постах время от времени.

3. Сборщики потоков

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

3.1. Собрать элементы потока в список

В данном примере сначала мы создаем поток целых чисел от 1 до 10. Затем мы обрабатываем элементы потока, чтобы найти все четные числа.

Наконец, мы собираем все четные числа в список.

List<Integer> list = new ArrayList<Integer>();for(int i = 1; i< 10; i++){list.add(i);}Stream<Integer> stream = list.stream();List<Integer> evenNumbersList = stream.filter(i -> i%2 == 0).collect(Collectors.toList());System.out.print(evenNumbersList);

3.2. Собрать элементы потока в массив

Данный пример похож на первый пример, показанный выше. Единственное отличие в том, что мы собираем четные числа в массив.

List<Integer> list = new ArrayList<Integer>();for(int i = 1; i< 10; i++){list.add(i);}Stream<Integer> stream = list.stream();Integer[] evenNumbersArr = stream.filter(i -> i%2 == 0).toArray(Integer[]::new);System.out.print(evenNumbersArr);

Есть много других способов также собирать поток в Set, Map или в несколько способов. Просто пройдите класс Collectors и постарайтесь запомнить их.

4. Потоковые операции

Абстракция потока имеет длинный список полезных функций. Давайте рассмотрим некоторые из них.

Прежде чем двигаться дальше, давайте заранее создадим Список строк. Мы построим наши примеры на основе этого списка, чтобы их было легко соотносить и понимать.

List<String> memberNames = new ArrayList<>();memberNames.add("Amitabh");memberNames.add("Shekhar");memberNames.add("Aman");memberNames.add("Rahul");memberNames.add("Shahrukh");memberNames.add("Salman");memberNames.add("Yana");memberNames.add("Lokesh");

Эти основные методы разделены на 2 части, представленные ниже:

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

Промежуточные операции возвращают сам поток, поэтому вы можете сцепить несколько вызовов методов подряд. Давайте изучим важные из них.

4.1.1.Поток.фильтр()

Метод filter() принимает Predicate для фильтрации всех элементов потока. Эта операция является промежуточной, что позволяет нам вызывать другую операцию потока(например, forEach() ) для результата.

memberNames.stream().filter((s) -> s.startsWith("A")).forEach(System.out::println);

Вывод программы:

AmitabhAman

4.1.2.Stream.map()

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

Следующий пример преобразует каждую строку в строку ВЕРХНЕГО РЕГИСТРА. Но мы можем использовать map() для преобразования объекта и в другой тип.

memberNames.stream().filter((s) -> s.startsWith("A")).map(String::toUpperCase).forEach(System.out::println);

Вывод программы:

AMITABHAMAN

4.1.2.Stream.sorted()

Метод sorted() — это промежуточная операция, которая возвращает отсортированное представление потока. Элементы в потоке сортируются в естественном порядке, если только мы не передадим пользовательский Comparator.

memberNames.stream().sorted().map(String::toUpperCase).forEach(System.out::println);

Вывод программы:

AMANAMITABHLOKESHRAHULSALMANSHAHRUKHSHEKHARYANA

Обратите внимание, что метод sorted() создает только отсортированное представление потока, не манипулируя порядком исходной коллекции. В этом примере порядок строки в memberNames не изменяется.

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

Терминальные операции возвращают результат определенного типа после обработки всех элементов потока.

После вызова терминальной операции на потоке начнется итерация потока и любого из связанных потоков. После завершения итерации возвращается результат терминальной операции.

4.2.1.Stream.forEach()

Метод forEach() помогает перебрать все элементы потока и выполнить некоторую операцию над каждым из них. Операция, которая должна быть выполнена, передается как лямбда-выражение.

memberNames.forEach(System.out::println);

4.2.2. Stream.collect()

Метод collect() используется для получения элементов из Steam и сохранения их в коллекции.

List<String> memNamesInUppercase = memberNames.stream().sorted().map(String::toUpperCase).collect(Collectors.toList());System.out.print(memNamesInUppercase);

Вывод программы:

[AMAN, AMITABH, LOKESH, RAHUL, SALMAN, SHAHRUKH, SHEKHAR, YANA]

4.2.3.Stream.match()

Различные операции сопоставления могут использоваться для проверки соответствия заданного предиката элементам потока. Все эти операции сопоставления являются конечными и возвращают логический результат.

boolean matchedResult = memberNames.stream().anyMatch((s) -> s.startsWith("A"));System.out.println(matchedResult); //truematchedResult = memberNames.stream().allMatch((s) -> s.startsWith("A"));System.out.println(matchedResult); //falsematchedResult = memberNames.stream().noneMatch((s) -> s.startsWith("A"));System.out.println(matchedResult); //false

4.2.4.Stream.count()

Count() — это терминальная операция, возвращающая количество элементов в потоке в виде длинного значения.

long totalMatched = memberNames.stream().filter((s) -> s.startsWith("A")).count();System.out.println(totalMatched); //2

4.2.5.Stream.reduce()

Метод reduce() выполняет сокращение элементов потока с помощью заданной функции. Результатом является Optional, содержащий сокращенное значение.

В данном примере мы сокращаем все строки, объединяя их с помощью разделителя #.

Optional<String> reduced = memberNames.stream().reduce((s1,s2) -> s1 + "#" + s2);reduced.ifPresent(System.out::println);

Вывод программы:

Amitabh#Shekhar#Aman#Rahul#Shahrukh#Salman#Yana#Lokesh

5. Короткое замыкание

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

Во внешней итерации мы будем использовать блок if-else. Во внутренних итерациях, таких как потоки, есть определенные методы, которые мы можем использовать для этой цели.

5.1.Stream.anyMatch()

AnyMatch() вернет true, как только условие, переданное как предикат, будет выполнено. После того, как будет найдено соответствующее значение, в потоке больше не будут обрабатываться элементы.

В приведенном примере, как только будет найдена строка, начинающаяся с буквы «А», поток завершится и будет возвращен результат.

boolean matched = memberNames.stream().anyMatch((s) -> s.startsWith("A"));System.out.println(matched); //true

5.2.Stream.findFirst()

Метод findFirst() вернет первый элемент из потока, после чего не будет обрабатывать больше никаких элементов.

String firstMatchedName = memberNames.stream().filter((s) -> s.startsWith("L")).findFirst().get();System.out.println(firstMatchedName); //Lokesh

6. Параллельные потоки

Благодаря фреймворку Fork/Join, добавленному в Java SE 7, у нас есть эффективный механизм для реализации параллельных операций в наших приложениях.

Но реализация фреймворка fork/join — сложная задача, и если ее не сделать правильно, она станет источником сложных многопоточных ошибок, которые могут привести к сбою приложения. С введением внутренних итераций мы получили возможность выполнять операции параллельно более эффективно.

Чтобы включить параллелизм, нам нужно всего лишь создать параллельный поток вместо последовательного. И к нашему удивлению, это действительно очень просто.

В любом из перечисленных выше примеров потоков, всякий раз, когда мы хотим выполнить определенную работу, используя несколько потоков в параллельных ядрах, нам нужно просто вызвать метод parallelStream() вместо метода stream().

List<Integer> list = new ArrayList<Integer>();for(int i = 1; i< 10; i++){list.add(i);}//Here creating a parallel streamStream<Integer> stream = list.parallelStream();Integer[] evenNumbersArr = stream.filter(i -> i%2 == 0).toArray(Integer[]::new);System.out.print(evenNumbersArr);

Ключевым драйвером для Stream API является повышение доступности параллелизма для разработчиков. Хотя платформа Java уже обеспечивает сильную поддержку параллелизма и параллелизма, разработчики сталкиваются с ненужными препятствиями при миграции своего кода с последовательного на параллельный по мере необходимости.

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

Также важно найти баланс между упрощением параллелизма и не заходить так далеко, чтобы сделать его невидимым. Прозрачность параллелизма приведет к недетерминизму и возможности гонок данных там, где пользователи этого не ожидают.

7. Методы потока

7.1 Создание потоков

  • concat()
  • пустой()
  • генерировать()
  • итерация()
  • из()

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

  • фильтр()
  • карта()
  • flatMap()
  • отчетливый()
  • сортировано()
  • заглянуть()
  • предел()
  • пропускать()

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

  • forEach()
  • forEachOrdered()
  • toArray()
  • уменьшать()
  • собирать()
  • мин()
  • макс()
  • считать()
  • anyMatch()
  • allMatch()
  • noneMatch()
  • findFirst()
  • findAny()

Исходный код на Github

Прокрутить вверх