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

Поток данных  —  одна из важнейших возможностей 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

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

Читайте нас в TelegramVK и Яндекс.Дзен


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

Предыдущая статья6 принципов создания производительных веб-приложений
Следующая статья5 самых полезных приемов в JavaScript