Java Streams против Vavr Streams
1. Вступление
В этой статье мы рассмотрим, чем реализацииStream отличаются в Java и Vavr.
В этой статье предполагается, что вы знакомы с основами какJava Stream API, так иthe Vavr library.
2. сравнение
Обе реализации представляют одну и ту же концепцию ленивых последовательностей, но отличаются в деталях.
Java Streams were built with robust parallelism in mind, обеспечивая простую поддержку распараллеливания. С другой стороны, реализация Vavr поддерживает удобную работу с последовательностями данных и не обеспечивает встроенной поддержки параллелизма (но этого можно добиться путем преобразования экземпляра в реализацию Java).
Вот почему потоки Java поддерживаются экземплярамиSpliterator - обновление до гораздо более старой версииIterator и Vavr поддерживается вышеупомянутымIterator (по крайней мере, в одной из последних реализаций).
Обе реализации слабо связаны с его поддерживающей структурой данных и, по сути, являются фасадом над источником данных, который проходит поток, но поскольку реализация Vavr основана наIterator-based _, _ она не допускает одновременных изменений исходной коллекции.
Обработка потоковых источников в Java делает возможным изменениеwell-behaved stream sources in до того, как будет выполнена операция терминального потока.
Несмотря на принципиальное отличие конструкции, Vavr предоставляет очень надежный API, который преобразует свои потоки (и другие структуры данных) в реализацию Java.
3. Дополнительная функциональность
Подход к работе с потоками и их элементами приводит к интересным различиям в способах работы с ними как в Java, так и в Vavr.
3.1. Случайный доступ к элементам
Предоставление удобных API и методов доступа к элементам - это одна из областей, в которой Vavr действительно сияет над Java API. Например, в Vavr есть несколько методов, обеспечивающих произвольный доступ к элементам:
-
get() обеспечивает доступ к элементам потока на основе индекса.
-
indexOf() обеспечивает ту же функциональность индексации, что и в стандартной JavaList.
-
insert() предоставляет возможность добавить элемент в поток в указанной позиции.
-
intersperse() вставит указанный аргумент между всеми элементами потока.
-
find() найдет и вернет элемент из потока. Java предоставляетnoneMatched, который просто проверяет наличие элемента.
-
update() will заменит элемент по заданному индексу. Это также принимает функцию для вычисления замены.
-
search() найдет элемент в отсортированном потоке (несортированные потоки дадут неопределенный результат)
Важно помнить, что эта функциональность по-прежнему поддерживается структурой данных, которая имеет линейную производительность для поиска.
3.2. Параллелизм и одновременное изменение
Хотя потоки Vavr изначально не поддерживают параллелизм, как методparallel() в Java, существует методtoJavaParallelStream , который предоставляет распараллеленную копию исходного потока Vavr на основе Java.
Область относительной слабости в потоках Vavr находится по принципуNon-Interference.
Проще говоря, потоки Java позволяют нам изменять базовый источник данных вплоть до вызова терминальной операции. Пока операция терминала не вызывается для данного потока Java, поток может принимать любые изменения в базовом источнике данных:
List intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
Stream intStream = intList.stream(); //form the stream
intList.add(5); //modify underlying list
intStream.forEach(i -> System.out.println("In a Java stream: " + i));
Мы обнаружим, что последнее добавление отражается в выходных данных потока. Это поведение согласуется, является ли модификация внутренней или внешней по отношению к потоковому конвейеру:
in a Java stream: 1
in a Java stream: 2
in a Java stream: 3
in a Java stream: 5
Мы обнаруживаем, что поток Вавр этого не потерпит:
Stream vavrStream = Stream.ofAll(intList);
intList.add(5)
vavrStream.forEach(i -> System.out.println("in a Vavr Stream: " + i));
Что мы получаем:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at io.vavr.collection.StreamModule$StreamFactory.create(Stream.java:2078)
Потоки Vavr не «хорошо себя ведут» по стандартам Java. Вавр лучше ведет себя с примитивными структурами данных поддержки:
int[] aStream = new int[]{1, 2, 4};
Stream wrapped = Stream.ofAll(aStream);
aStream[2] = 5;
wrapped.forEach(i -> System.out.println("Vavr looped " + i));
Давать нам:
Vavr looped 1
Vavr looped 2
Vavr looped 5
3.3. Операции короткого замыкания иflatMap()
flatMap,, как и операцияmap, является промежуточной операцией в потоковой обработке - обе реализации следуют заthe contract of intermediate stream operations - обработка из базовой структуры данных не должна происходить до тех пор, пока не будет вызвана терминальная операция.
Однако в JDK 8 и 9 есть функцияa bug, которая заставляет реализациюflatMap разрывать этот контракт и быстро оценивать в сочетании с короткими промежуточными операциями, такими какfindFirst orlimit.
Простой пример:
Stream.of(42)
.flatMap(i -> Stream.generate(() -> {
System.out.println("nested call");
return 42;
}))
.findAny();
В приведенном выше фрагменте мы никогда не получим результат отfindAny, потому чтоflatMap будет оцениваться с нетерпением, вместо того, чтобы просто брать один элемент из вложенногоStream.
Исправление этой ошибки было предоставлено в Java 10.
У VavrflatMap нет такой проблемы, и функционально аналогичная операция завершается за O (1):
Stream.of(42)
.flatMap(i -> Stream.continually(() -> {
System.out.println("nested call");
return 42;
}))
.get(0);
3.4. Основные функции Vavr
В некоторых областях просто нет однозначного сравнения Java и Vavr; Vavr расширяет возможности потоковой передачи с помощью функциональности, которая напрямую не имеет себе равных в Java (или, по крайней мере, требует значительного количества ручной работы):
-
zip() объединяет элементы в потоке с элементами из предоставленногоIterable. Эта операция раньше поддерживалась в JDK-8, но имеетsince been removed after build-93
-
partition() will разбивает содержимое потока на два потока, учитывая предикат.
-
permutation() as с именем, вычислит перестановку (все возможные уникальные порядки) элементов потока.
-
combinations() дает комбинацию (т.е. возможен выбор предметов) из потока.
-
groupBy вернет потокиMap of, содержащие элементы из исходного потока, классифицированные предоставленным классификатором.
-
Методdistinct в Vavr улучшен по сравнению с версией Java, предоставляя вариант, который принимает лямбда-выражениеcompareTo.
Хотя поддержка расширенных функций в потоках Java SE несколько не вдохновляет,Expression Language 3.0, как ни странно, обеспечивает поддержку гораздо большей функциональности, чем стандартные потоки JDK.
4. Управление потоком
Vavr позволяет напрямую манипулировать содержимым потока:
-
Вставить в существующий поток Vavr
Stream vavredStream = Stream.of("foo", "bar", "baz");
vavredStream.forEach(item -> System.out.println("List items: " + item));
Stream vavredStream2 = vavredStream.insert(2, "buzz");
vavredStream2.forEach(item -> System.out.println("List items: " + item));
-
Удалить элемент из потока
Stream removed = inserted.remove("buzz");
-
Операции на основе очередей
Поскольку поток Vavr поддерживается очередью, он обеспечивает операцииprepend иappend с постоянным временем.
Однакоchanges made to the Vavr stream don’t propagate back to the data source that the stream was created from.
5. Заключение
У Vavr и Java есть свои сильные стороны, и мы продемонстрировали приверженность каждой библиотеки поставленным задачам - Java для дешевого параллелизма и Vavr для удобных потоковых операций.
Благодаря поддержке Vavr преобразования между собственным потоком и Java-потоками, можно получить преимущества обеих библиотек в одном проекте без больших накладных расходов.
Исходный код этого руководства доступенover on Github.