Перейти к основному содержимому

Потоки Java против потоков Vavr

· 6 мин. чтения

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 .