Источник: 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.
Читайте также:
Перевод статьи Jordan Williams: Don’t Use Java For Loops — Consider Java Streams Instead