1. Введение
В этой статье мы рассмотрим, чем отличаются реализации Stream в Java и Vavr.
В этой статье предполагается знакомство с основами как Java Stream API , так и библиотеки Vavr .
2. Сравнение
Обе реализации представляют одну и ту же концепцию ленивых последовательностей, но отличаются в деталях.
Потоки
Java были созданы с учетом надежного параллелизма , обеспечивая простую поддержку параллелизма. С другой стороны, реализация Vavr способствует удобной работе с последовательностями данных и не обеспечивает встроенной поддержки параллелизма (но этого можно добиться путем преобразования экземпляра в реализацию Java).
Вот почему потоки Java поддерживаются экземплярами Spliterator
— обновление гораздо более старого Iterator
, а реализация Vavr поддерживается вышеупомянутым Iterator
(по крайней мере, в одной из последних реализаций).
Обе реализации слабо привязаны к своей поддерживающей структуре данных и, по сути, являются фасадами поверх источника данных, через которые проходит поток, но поскольку реализация Vavr основана на итераторе
, она
не допускает одновременных модификаций исходной коллекции.
Обработка источников потока в Java позволяет модифицировать хорошо работающие источники потока до того, как будет выполнена операция терминального потока.
Несмотря на принципиальную разницу в дизайне, Vavr предоставляет очень надежный API, который преобразует свои потоки (и другие структуры данных) в реализацию Java.
3. Дополнительный функционал
Подход к работе с потоками и их элементами приводит к интересным различиям в способах работы с ними как в Java, так и в Vavr.
3.1. Произвольный доступ к элементам
Предоставление удобного API и методов доступа к элементам — это одна из областей, в которой Vavr действительно превосходит Java API. Например, в Vavr есть несколько методов, обеспечивающих произвольный доступ к элементам:
get()
обеспечивает доступ на основе индекса к элементам потока.indexOf()
обеспечивает ту же функциональность расположения индекса, что и в стандартномсписке Java.
insert()
предоставляет возможность добавить элемент в поток в указанной позиции.intersperse()
вставит предоставленный аргумент между всеми элементами потока.find()
найдет и вернет элемент из потока. Java предоставляетnoneMatched
, который просто проверяет существование элемента.update()
заменит элемент по заданному индексу. Это также принимает функцию для вычисления замены.search
()
найдет элемент в отсортированном `` потоке (несортированные потоки дадут неопределенный результат)
Важно помнить, что эта функциональность по-прежнему поддерживается структурой данных, которая имеет линейную производительность для поиска.
3.2. Параллелизм и параллельная модификация
Хотя потоки Vavr изначально не поддерживают параллелизм, как метод parallel()
в Java , существует метод toJavaParallelStream
, который предоставляет распараллеленную копию исходного потока Vavr на основе Java.
Область относительной слабости в потоках Vavr основана на принципе невмешательства
.
Проще говоря, ** ** потоки Java позволяют нам изменять базовый источник данных вплоть до вызова терминальной операции. Пока терминальная операция не была вызвана для данного потока Java, поток может улавливать любые изменения в базовом источнике данных:
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
Stream<Integer> 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
Мы обнаруживаем, что поток Vavr этого не допустит:
Stream<Integer> 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. Vavr лучше ведет себя с примитивными базовыми структурами данных:
int[] aStream = new int[]{1, 2, 4};
Stream<Integer> 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 ,
как и операция карты
, является промежуточной операцией в потоковой обработке — обе реализации следуют контракту промежуточных потоковых операций — обработка из базовой структуры данных не должна происходить до тех пор, пока не будет вызвана терминальная операция.
В JDK 8 и 9, однако, есть ошибка , из-за которой реализация flatMap
нарушает этот контракт и с готовностью оценивает в сочетании с короткими промежуточными операциями, такими как findFirst
или limit
.
Простой пример:
Stream.of(42)
.flatMap(i -> Stream.generate(() -> {
System.out.println("nested call");
return 42;
}))
.findAny();
В приведенном выше фрагменте мы никогда не получим результат от findAny
, потому что flatMap
будет оцениваться с нетерпением, вместо того, чтобы просто взять один элемент из вложенного потока.
Исправление этой ошибки было предоставлено в Java 10.
FlatMap от Vavr
не имеет такой проблемы, и функционально аналогичная операция завершается за 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, но с тех пор была удалена после сборки-93 .partition()
разделит содержимое потока на два потока с учетом предиката.permutation()
, как указано, будет вычислять перестановку (все возможные уникальные порядки) элементов потока.комбинации()
дает комбинацию (т.е. возможный выбор элементов) потока.groupBy
вернеткарту
потоков, содержащую элементы из исходного потока, классифицированные предоставленным классификатором.Отдельный метод в Vavr
улучшает версию Java, предоставляя вариант, который принимает лямбда-выражениеcompareTo .
В то время как поддержка расширенной функциональности в потоках Java SE несколько скучна, Expression Language 3.0 странным образом обеспечивает поддержку гораздо большей функциональности, чем стандартные потоки JDK.
4. Управление потоком
Vavr позволяет напрямую манипулировать содержимым потока:
- Вставить в существующий поток Vavr
Stream<String> vavredStream = Stream.of("foo", "bar", "baz");
vavredStream.forEach(item -> System.out.println("List items: " + item));
Stream<String> vavredStream2 = vavredStream.insert(2, "buzz");
vavredStream2.forEach(item -> System.out.println("List items: " + item));
- Удалить элемент из потока
Stream<String> removed = inserted.remove("buzz");
- Операции на основе очереди
Поскольку поток Vavr поддерживается очередью, он обеспечивает операции добавления
и добавления с постоянным временем.
Однако изменения, внесенные в поток Vavr, не распространяются обратно на источник данных, из которого был создан поток.
5. Вывод
И у Vavr, и у Java есть свои сильные стороны, и мы продемонстрировали приверженность каждой библиотеки своим целям разработки: Java — дешевому параллелизму, а Vavr — удобным потоковым операциям.
С поддержкой Vavr для преобразования туда и обратно между своим собственным потоком и потоком Java можно использовать преимущества обеих библиотек в одном проекте без больших накладных расходов.
Исходный код этого руководства доступен на Github .