Java 8 и бесконечные потоки

Java 8 и бесконечные потоки

1. обзор

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

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

Эта лень достигается разделением между двумя типами операций, которые могут выполняться в потоках: операцииintermediate иterminal.

2. Промежуточные и терминальные операции

All Stream operations are divided into intermediate and terminal operations и объединяются для формирования потоковых трубопроводов.

Потоковый конвейер состоит из источника (например,Collection, массива, функции генератора, канала ввода-вывода или генератора бесконечной последовательности); за которыми следуют ноль или более промежуточных операций и терминальная операция.

2.1. Intermediate Операции

ОперацииIntermediate не выполняются, если вызывается какая-либо операцияterminal.

Они составляют конвейер выполненияStream. Операциюintermediate можно добавить в конвейерStream с помощью следующих методов:

  • фильтр()

  • карта()

  • flatMap ()

  • различны ()

  • отсортированный ()

  • PEEK ()

  • предел ()

  • пропускать()

Все операцииIntermediate ленивые, поэтому они не выполняются до тех пор, пока результат обработки не понадобится.

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

Таким образом, обходStream не начинается до тех пор, пока не будет выполнена операция конвейераterminal.

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

2.2. Terminal Операции

ОперацииTerminal могут пересекать поток, чтобы произвести результат или побочный эффект.

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

Активность терминальной операции важна для бесконечных потоков, потому чтоat the moment of processing we need to think carefully if our Stream is properly bounded by, например, преобразованиеlimit(). Terminal операции:

  • для каждого()

  • forEachOrdered ()

  • ToArray ()

  • уменьшения ()

  • собирать ()

  • мин ()

  • Максимум()

  • кол-()

  • anyMatch ()

  • allMatch ()

  • noneMatch ()

  • FindFirst ()

  • findAny ()

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

3. Бесконечные потоки

Теперь, когда мы понимаем эти две концепции - операцииIntermediate иTerminal - мы можем написать бесконечный поток, который использует лень Streams.

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

It is crucial to use a limit() method before executing a collect() method, это терминальная операция, иначе наша программа будет работать бесконечно:

// given
Stream infiniteStream = Stream.iterate(0, i -> i + 2);

// when
List collect = infiniteStream
  .limit(10)
  .collect(Collectors.toList());

// then
assertEquals(collect, Arrays.asList(0, 2, 4, 6, 8, 10, 12, 14, 16, 18));

Мы создали бесконечный поток с помощью методаiterate(). Затем мы вызвали преобразованиеlimit() и терминальную операциюcollect(). Тогда в нашем результирующемList, у нас будут первые 10 элементов бесконечной последовательности из-за ленивости aStream.

4. Бесконечный поток элементов произвольного типа

Допустим, мы хотим создать бесконечный поток случайныхUUIDs.

Первым шагом к достижению этого с помощью APIStream является созданиеSupplier этих случайных значений:

Supplier randomUUIDSupplier = UUID::randomUUID;

Когда мы определяем поставщика, мы можем создать бесконечный поток, используя методgenerate():

Stream infiniteStreamOfRandomUUID = Stream.generate(randomUUIDSupplier);

Тогда мы могли бы взять пару элементов из этого потока. Нам нужно не забыть использовать методlimit(), если мы хотим, чтобы наша программа завершилась за конечное время:

List randomInts = infiniteStreamOfRandomUUID
  .skip(10)
  .limit(10)
  .collect(Collectors.toList());

Мы используем преобразованиеskip(), чтобы отбросить первые 10 результатов и взять следующие 10 элементов. Мы можем создать бесконечный поток элементов любого настраиваемого типа, передав функцию интерфейсаSupplier методуgenerate() наStream.

6. Do-While - Путь потока

Допустим, в нашем коде есть простой цикл do.. while:

int i = 0;
while (i < 10) {
    System.out.println(i);
    i++;
}

Мы печатаем счетчикi десять раз. Мы можем ожидать, что такую ​​конструкцию можно будет легко написать с использованием APIStream и, в идеале, у нас будет методdoWhile() в потоке.

К сожалению, в потоке такого метода нет, и когда мы хотим достичь функциональности, аналогичной стандартному циклуdo-while, нам нужно использовать методlimit():

Stream integers = Stream
  .iterate(0, i -> i + 1);
integers
  .limit(10)
  .forEach(System.out::println);

Мы достигли той же функциональности, что и императивный цикл while, с меньшим количеством кода, но вызов функцииlimit() не так нагляден, как если бы у нас был методdoWhile() для объектаStream.

5. Заключение

В этой статье объясняется, как мы можем использоватьStream API для создания бесконечных потоков. Они, при использовании вместе с преобразованиями, такими какlimit() –, могут значительно упростить понимание и реализацию некоторых сценариев.

Код, поддерживающий все эти примеры, можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.