Потоки Java 14. Методы и операции фабрики потоков
Технически все они являются методами, принадлежащими интерфейсу Stream. Но некоторые из них создают объект Stream (мы называем их фабричные методы или просто методы; все они статические), а другие обрабатывают элементы испускаются созданным объектом Stream (мы называем их операциями; все они нестатичны).
Все операции делятся на промежуточные операции и терминальные операции. Промежуточные операции возвращают объект Stream и, таким образом, позволяют применять другие операции к возвращаемому объекту Stream. Терминальные операции либо возвращают значения других типов, либо вообще ничего не возвращают (вызывают только побочные эффекты). Они не позволяют применять другие операции и закрывают поток.
Методы фабрики потоков
Ниже приведены фабричные методы интерфейса Stream:
static Stream<T> generate(Supplier<T> s);
static Stream<T> iterate(T seed, UnaryOperator<T> f);
static Stream<T> iterate(T seed, Predicate<T> hasNext,
UnaryOperator<T> next);
static Stream.Builder<T> builder();
static Stream<T> of(T... values);
static Stream<T> empty();
static Stream<T> ofNullable(T t);
static Stream<T> concat(Stream<T> a, Stream<T> b);
Первые пять мы уже обсуждали в этой серии:
static Stream‹T› generate(Supplier‹T› s);
static Stream‹T› iterate(T seed, UnaryOperator‹T› f);
static Stream‹T› iterate(T seed, Predicate‹T› hasNext, UnaryOperator‹T› next);
static Stream.Builder‹T› builder();
static Stream‹T› of(T… values);
Остальные три — empty(), ofNullable() и concat() — мы обсудим в следующей статье Фабрика потоков. методы.
Потоковая передача промежуточных операций
Ниже приведены промежуточные операции, которые сортируют, пропускают и фильтруют элементы, создаваемые объектом Stream:
Stream<T> sorted();
Stream<T> distinct();
Stream<T> skip(long n);
Stream<T> limit(long maxSize);
Stream<T> filter(Predicate<T> predicate);
Stream<T> sorted(Comparator<T> comparator);
default Stream<T> dropWhile(Predicate<T> predicate);
default Stream<T> takeWhile(Predicate<T> predicate);
Ниже приведены промежуточные операции, преобразующие элементы потока (сопоставлять каждый из них с другим значением с помощью предоставленной функции):
Stream<R> map(Function<T,R> mapper);
IntStream mapToInt(ToIntFunction<T> mapper);
LongStream mapToLong(ToLongFunction<T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<T> mapper);
А остальное — это промежуточные операции, которые генерируют другой поток из каждого исходного элемента потока с помощью предоставленной функции и встраивают этот новый поток в исходный поток элементов:
Stream<R> flatMap(Function<T, Stream<R>> mapper);
IntStream flatMapToInt(Function<T,IntStream> mapper);
LongStream flatMapToLong(Function<T,LongStream> mapper);
DoubleStream flatMapToDouble(Function<T,DoubleStream> mapper);
Все вышеперечисленные промежуточные операции мы обсудим в следующих статьях.
Есть одна промежуточная операция, которая не преобразует ни один из элементов:
Stream<T> peek(Consumer<T> action);
Откуда мы знаем, что эта операция не изменяет элементы потока? Он использует функцию Consumer для выполнения своей работы, а функция Consumer ничего не возвращает (void). Ниже приведен пример использования операции peek():
List.of("1","2","3").stream()
.map(s -> s + "a")
.peek(System.out::println)
.map(s -> s + "b")
.forEach(System.out::println);
Вывод приведенного выше фрагмента кода выглядит следующим образом:
1a
1ab
2a
2ab
3a
3ab
Операция peek() обычно используется для отладки, чтобы проверить промежуточный результат.
Потоковые операции терминала
Ниже приведены все операции терминала Stream:
long count(); void forEach(Consumer<T> action); void forEachOrdered(Consumer<T> action);
boolean allMatch(Predicate<T> predicate); boolean anyMatch(Predicate<T> predicate); boolean noneMatch(Predicate<T> predicate);
Optional<T> findAny(); Optional<T> findFirst(); Optional<T> max(Comparator<T> comparator); Optional<T> min(Comparator<T> comparator);
Object[] toArray(); A[] toArray(IntFunction<A[]> generator);
Optional<T> reduce(BinaryOperator<T> accumulator); T reduce(T identity, BinaryOperator<T> accumulator); U reduce(U identity, BiFunction<U,T,U> accumulator, BinaryOperator<U> combiner);
R collect(Collector<T,A,R> collector); R collect(Supplier<R> supplier, BiConsumer<R,T> accumulator, BiConsumer<R,R> combiner);
Операция count() проста. Он возвращает количество элементов, испускаемых потоком. Естественно, имеет смысл использовать только с конечными потоками. В противном случае он никогда не возвращает никакого значения. Вот пример его использования:
long c = List.of("a","b","c").stream().count();
System.out.print(c); //prints: 3
Пример использования операции forEach() вы видели в нашем предыдущем примере. Он производит побочный эффект, используя предоставленную функцию, и ничего не возвращает. Его можно использовать с бесконечным потоком.
Операция forEachOrdered() полезна, когда обработка потока происходит параллельно (подробнее о параллельной обработке мы поговорим в одном из следующих постов). Чтобы продемонстрировать разницу между операциями forEach() и forEachOrdered(), воспользуемся ими для параллельной обработки одного и того же потока:
List.of("1","2","3","4") .parallelStream() .forEach(System.out::print); //prints: 3421
List.of("1","2","3","4") .parallelStream() .forEachOrdered(System.out::print); //prints: 1234
Как видите, операция forEachOrdered() обрабатывает элементы потока в том порядке, в котором они были отправлены потоком, а операция forEach() обрабатывает их в том порядке, в котором получает элементы из подпотоков, обрабатываемых параллельно, поэтому порядок обработки может меняться при каждом запуске.
Другие операции с терминалом мы обсудим в следующих постах.
В следующем посте мы поговорим о трех фабричных методах — empty(), ofNullable() и concat().
См. также другие сообщения о потоках Java 8.