Введение в Streams API

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

  1. Потоки Java спроектированы таким образом, что большинство потоковых операций (называемых промежуточными операциями) возвращают поток. Это помогает создать цепочку потоковых операций. Это называется stream pipe-lining.
  2. Потоки Java также поддерживают агрегатные или терминальные операции над элементами. Агрегатные операции — это операции, которые позволяют нам быстро и четко выражать общие манипуляции с элементами потока, например, находить максимальный или минимальный элемент, находить первый элемент, соответствующий заданным критериям, и так далее.
  3. Поток поддерживает тот же порядок элементов, что и порядок в источнике потока.
  1. Что такое Поток? Поток против коллекции?
  2. Создание потоков
  3. Коллекторы потоков
  4. Потоковые операции
  5. Операции замыкания
  6. Параллельные потоки
  7. Другие операции

Что такое Поток? Поток или коллекции?

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

На очень высоком уровне мы можем рассматривать небольшие фрагменты видеофайла как поток, а все видео — как коллекцию.

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

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

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

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

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

Потоки создаются в исходном коде, например в классе java.util.Collection, для List or Set. Map напрямую не поддерживается, мы можем только создать поток ключей или значений.

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

Основываясь на вышеуказанных пунктах, поток это :

  1. Не структура данных
  2. Разработан под лямбды
  3. Не поддерживает индексированный доступ
  4. Может быть легко объединен в виде массивов или списков
  5. Поддерживается отложенный доступ
  6. Распараллеливаемый

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

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

Stream.of()

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

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

List.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));

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

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

Stream<Integer> randomNumbers = Stream
      .generate(() -> (new Random()).nextInt(100));

randomNumbers.limit(20).forEach(System.out::println);

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

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

IntStream stream = "12345_abcdefg".chars();
stream.forEach(p -> System.out.println(p));

//OR

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

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

Коллекторы потоков

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

Collect Stream elements to a List

В данном примере, во-первых, мы создаем поток для целых чисел от 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);

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

Приведенный пример аналогичен первому примеру, показанному выше. Единственное отличие заключается в том, что мы собираем четные числа в массив.

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);

Существует также множество других способов собрать поток в SetMap или что то другое. Просто пройдитесь по классу Collectors и постарайтесь помнить о них.

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

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

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

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 части, приведенные ниже:

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

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

Stream.filter()

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

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

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

Amitabh
Aman

Stream.map()

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

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

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

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

AMITABH
AMAN

Stream.sorted()

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

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

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

AMAN
AMITABH
LOKESH
RAHUL
SALMAN
SHAHRUKH
SHEKHAR
YANA

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

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

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

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

Stream.forEach()

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

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

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]

Stream.match()

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

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

Stream.count()

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

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

Stream.reduce()

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

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

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

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

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

Операции замыкания

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

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

Stream.anyMatch()

Функция any Match() вернет значение true, как только условие, переданное в качестве предиката, удовлетворит. Как только будет найдено совпадающее значение, в потоке больше не будут обрабатываться элементы.

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

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

Stream.findFirst()

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

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

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

С фреймворком 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 stream
Stream<Integer> stream = list.parallelStream();  

Integer[] evenNumbersArr = stream.filter(i -> i%2 == 0).toArray(Integer[]::new);
System.out.print(evenNumbersArr);

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

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

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

Другие операции

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

  • concat()
  • empty()
  • generate()
  • iterate()
  • of()

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

  • filter()
  • map()
  • flatMap()
  • distinct()
  • sorted()
  • peek()
  • limit()
  • skip()

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

  • forEach()
  • forEachOrdered()
  • toArray()
  • reduce()
  • collect()
  • min()
  • max()
  • count()
  • anyMatch()
  • allMatch()
  • noneMatch()
  • findFirst()
  • findAny()

Добавить комментарий

Ваш адрес email не будет опубликован.

два + девятнадцать =