Поток данных — одна из важнейших возможностей 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
.
Читайте также:
- Когда параллелизм превосходит конкурентность
- Незаслуженно забытый ForkJoinPool
- Очереди с приоритетом в Java
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Jordan Williams: Don’t Use Java For Loops — Consider Java Streams Instead