Java Streams против Vavr Streams

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.