Найти в Дзене
Nuances of programming

Циклы Java в сторону - даешь потоки!

Оглавление

Источник: Nuances of Programming

Поток данных  —  одна из важнейших возможностей Java 8. Чтобы узнать все ее преимущества, необходимо понять ее суть.

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

Рассмотрим пример.

Сначала создадим список случайных чисел:

List<Integer> list = new ArrayList<Integer>();
Random random = new Random();
for (int i = 0; i < 10; i++) {
int rand = random.nextInt(10);
list.add(rand);
}

После этого отберем в новый список все числа больше 5:

List<Integer> filteredList = new ArrayList<Integer>();
for (Integer e :
list) {
if (e > 5) {
filteredList.add(e);
}
}

Вывод:

[8, 6, 8, 6]

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

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

Stream<Integer> randStream = Stream.generate(()-> new Random().nextInt(10)).limit(10);

List<Integer> filteredList = randStream.filter(e -> e > 5).collect(Collectors.toList());

Вывод:

[7, 8, 6]

Обратите внимание, что размер списка изменился по причине случайного характера чисел.

Очевидно, что нам удалось написать гораздо меньше кода и получить более простой результат.

Сначала вышеуказанный код генерирует список случайных чисел посредством Stream.generate. Применяя limit(10), мы ограничили их количество до 10. В результате получили поток целых чисел (Stream<Integer>).

Затем с помощью filter из этого потока были отобраны все элементы больше 5.

Как видно, стало меньше кода и намного больше логики по сравнению с циклами for.

Объединим эти две операции:

List<Integer> filteredList = Stream.generate(() -> new Random().nextInt(10)).
limit(10).filter(e -> e > 5).map(e -> e * 2).collect(Collectors.toList());

Примечание. () ->  —  это по большей части Java-версии анонимных функций (лямбда-выражений), которые были представлены в Java 8 наряду с потоками. При наличии только одного параметра скобки не нужны (наподобие e -> e > 5). Их реализация аналогична стрелочным функциям JavaScript.

Потоки очень эффективны при программировании крупных приложений.

А теперь переходим к изучению основных операций с ними.

Основные операции с потоками

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

Метод Generate

Stream<String> streamGenerated =
Stream.
generate(() -> "value");

Обращение к методу .generate класса Stream позволяет создать поток, состоящий из строки “value”.

Потоки предполагают отложенную загрузку (можно назвать их коллекцией с отложенной загрузкой), т. е. они вычисляются по мере их необходимости. В связи с этим обозначилось одно интересное свойство: за объявлением Stream<String> не последовало выполнение ни одной операции.

Это необходимо, поскольку Stream.generate создает бесконечное количество значений. В данном случае бесконечное число раз было сгенерировано “value”. Однако с учетом отложенной загрузки оно не вычисляется.

Теперь ограничим этот поток до 20 чисел, добавив метод .limit:

Stream<String> streamGenerated =
Stream.
generate(() -> "value").limit(10);

Потоки работают по шаблону Builder (Строитель), позволяющему связывать операторы друг с другом. .limit возвращает Stream. Операторы, возвращающие потоки, называются промежуточными. В сущности, они продолжают цепочку.

Для вычисления потоков необходимо добавить терминальный оператор. Он преобразует их в конкретный тип, например int, float, double, или коллекции (массивы, хэш-карты) для последующего использования программой.

Как правило, чаще всего поток преобразуется в коллекцию, такую как List.

Что мы сейчас и сделаем с помощью метода .collect.

List<String> streamToList = streamGenerated.collect(Collectors.toList());

Метод .collect принимает Collector, в зависимости от типа которого вы можете контролировать, как и во что преобразуется поток. В данном случае, это будет список.

Кратко перечислим примеры других операций Collector:

  • .toSet();
  • .toMap();
  • .joining().

Вывод streamToList :

[value, value, value, value, value, value, value, value, value, value, value, value, value, value, value, value, value, value, value, value]

А вот и требуемый список.

Метод Iterate

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

Stream<Integer> streamIterated = Stream.iterate(0, i -> i + 2).limit(10);

В качестве первого аргумента выступает исходное число, а второй отражает “принцип” выполняемой итерации.

Вывод:

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Структура .iterate напоминает структуру цикла for:

for (int i = 0; i < 20; i = i + 2) {
// код
}

Коллекции

Коллекции в Java позволяют хранить и управлять данными. Все из них можно напрямую преобразовывать в потоки. К коллекциям относятся три вида интерфейсов:

  • списки;
  • множества;
  • очереди.

Интерфейс List вам наверняка знаком. Различные его реализации состоят из ArrayList, Vector и Stack .

Предположим, у нас есть ArrayList целых чисел с именем arrayToStream. Его легко можно преобразовать в поток. Все перечисленные коллекции поддерживают подобное преобразование с помощью метода .stream:

Stream<Integer> stream = arrayToStream.stream();

Вот так все просто.

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

Главное преимущество потоков состоит в предоставлении множества таких операторов. Они обеспечивают более удобный подход к работе с циклами.

  • map;
  • filter;
  • distinct;
  • limit.

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

Рассмотрим некоторые из операторов.

map

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

Например, у нас есть список чисел от 1 до 10: [1,2,3,4,5,6,7,8,9,10]. Нужно возвести каждое из них в квадрат: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]. Как раз для этого и потребуется map.

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

Stream<Integer> numbers1To10Stream = numbers1To10.stream().map(num -> num * num);

Для преобразования в список потребуется .collect.

Без него вывод numbers1To10Stream выглядел бы примерно так java.util.stream.ReferencePipeline$3@3feba861, поскольку он еще не преобразован в нужный нам тип.

Вывод с .collect:

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

filter

filter действует согласно своему названию: выбирает элементы списка по заданному условию.

Например, работая с тем же списком чисел от 1 до 10 и отбирая все четные из них, получаем [2, 4, 6, 8, 10].

Stream<Integer> numbers1To10Stream = numbers1To10.stream().filter(num -> num % 2 == 0);

Для метода filter требуется функция, принимающая значение и определяющая, является оно true или false. Если true, то число попадает в список. При этом обязательным условием становится проверка способности любого входящего числа делиться на 2: num -> num % 2 == 0 .

Вывод:

[2, 4, 6, 8, 10]

distinct

Этот промежуточный оператор устраняет из потока все повторения.

Допустим, у нас есть список чисел [3, 3, 3, 3, 8, 8, 8, 8]. После удаления всех повторяющихся элементов он принимает такой вид [3, 8].

Stream<Integer> distinct = duplicatedList.stream().distinct();

Вывод:

[3, 8]

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

anyMatch

Если какой-либо элемент в потоке отвечает заданному условию, то данный оператор возвращает true, в противному случае  —  false.

На основе примера [3,4,7,9] проверим, есть в этом списке четные числа:

boolean anyMatchBool = anyMatchList.stream().anyMatch(num -> num % 2 == 0);

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

Вывод:

true

allMatch

Если все элементы в потоке отвечают заданному условию, то этот оператор возвращает true, иначе  —  false.

Рассмотрим пример [3,4,7,9]:

boolean anyMatchBool = allMatchList.stream().allMatch(num -> num % 2 == 0);

В качестве результата данного примера получаем false, поскольку в списке присутствуют нечетные числа.

А вот если бы список был [2,4,6,8], то оператор вернул бы true, так как все числа четные.

forEach

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

Пример [3,4,7,9]:

list.stream().forEach(num -> System.out.println(num));

Вывод:

3
4
7
9

В отличие от map оператор forEach не изменяет текущий список.

А теперь самое время для практики.

Задание

Необходимо получить список книг со статусом “Expired” (Срок возврата истек) и проверить, числятся ли какие-нибудь из них за Jordan:

boolean result = libraryRepo.stream().filter(b -> b.getStatus().equalsIgnoreCase("Expired")).
anyMatchList(b -> b.getBorrower().equalsIgnoreCase("Jordan"));

У нас есть библиотека, предоставляющая список книг. Из них были отобраны (filtered) все те, срок возврата которых истек. Затем мы проверили, а брал ли какие-либо из них Jordan. Если да, то возвращается true, в противном случае  —  false.

Читайте также:

Читайте нас в Telegram, VK

Перевод статьи Jordan Williams: Don’t Use Java For Loops — Consider Java Streams Instead