Как стать автором
Обновить

Руководство по API Коллекций Vavr

Время на прочтение14 мин
Количество просмотров8.4K
Автор оригинала: baeldung
VAVR (известная ранее, как Javaslang) — это некоммерческая функциональная библиотека для Java 8+. Она позволяет писать функциональный Scala-подобный код в Java и служит для уменьшения количества кода и повышения его качества. Сайт библиотеки.

Под катом — перевод статьи, систематизирующей информацию по API Коллекций Vavr.

Перевел @middle_java

Последнее изменение оригинальной статьи: 15 августа 2019 г.

1. Обзор


Библиотека Vavr, известная ранее, как Javaslang, является функциональной библиотекой для Java. В этой статье мы исследуем ее мощный API коллекций.

Дополнительные сведения об этой библиотеке см. в этой статье.

2. Персистентные Коллекции


Персистентная коллекция при изменении создает новую версию коллекции, не изменяя при этом текущую версию.

Поддержка нескольких версий одной коллекции может привести к неэффективному использованию ЦП и памяти. Однако, библиотека коллекций Vavr преодолевает это, расшаривая структуру данных между различными версиями коллекции.

Это принципиально отличается от unmodifiableCollection() из утилитного Java класса Collections, который просто предоставляет оболочку (wrapper) для базовой коллекции.

Попытка изменить такую коллекцию приводит к UnsupportedOperationException вместо создания новой версии. Более того, базовая коллекция по-прежнему изменяема через прямую ссылку на нее.

3. Traversable


Traversable — это базовый тип всех коллекций Vavr. Этот интерфейс определяет методы, общие для всех структур данных.

Он предоставляет некоторые полезные методы по-умолчанию, такие как size(), get(), filter(), isEmpty() и другие, которые наследуются суб-интерфейсами.

Исследуем далее библиотеку коллекций.

4. Seq


Начнем с последовательностей.

Интерфейс Seq представляет собой последовательные структуры данных. Это родительский интерфейс для List, Stream, Queue, Array, Vector и CharSeq. Все эти структуры данных имеют свои уникальные свойства, которые мы рассмотрим ниже.

4.1. List


List — это энергично вычисляемая (eagerly-evaluated, операция выполняется, как только становятся известны значения ее операндов) последовательность элементов, расширяющих интерфейс LinearSeq.

Персистентные List конструируются рекурсивно с использованием головы и хвоста:

  • Голова — первый элемент
  • Хвост — список, содержащий остальные элементы (этот список также формируется из головы и хвоста)

API List содержит статические фабричные методы, которые можно использовать для создания List. Можно использовать статический метод of() для создания экземпляра List из одного или нескольких объектов.

Можно также использовать статический метод empty() для создания пустого List и метод ofAll() для создания List из типа Iterable:

List < String > list = List.of(
    "Java", "PHP", "Jquery", "JavaScript", "JShell", "JAVA");

Рассмотрим некоторые примеры манипулирования списками.

Мы можем использовать метод drop() и его варианты для удаления первых N элементов:

List list1 = list.drop(2);
assertFalse(list1.contains("Java") && list1.contains("PHP"));

List list2 = list.dropRight(2);
assertFalse(list2.contains("JAVA") && list2.contains("JShell"));

List list3 = list.dropUntil(s - > s.contains("Shell"));
assertEquals(list3.size(), 2);

List list4 = list.dropWhile(s - > s.length() > 0);
assertTrue(list4.isEmpty());

drop(int n) удаляет n элементов из списка, начиная с первого элемента, в то время как dropRight() делает то же самое, начиная с последнего элемента в списке.

dropUntil() удаляет элементы из списка до тех пор, пока предикат не станет равен true, в то время как dropWhile() удаляет элементы, пока предикат равен true.

Также есть методы dropRightWhile() и dropRightUntil(), которые удаляют элементы, начиная справа.

Далее, take(int n) используется для извлечения элементов из списка. Он берет n элементов из списка, а затем останавливается. Существует также takeRight(int n), который берет элементы, начиная с конца списка:

List list5 = list.take(1);
assertEquals(list5.single(), "Java");

List list6 = list.takeRight(1);
assertEquals(list5.single(), "Java");

List list7 = list.takeUntil(s - > s.length() > 6);
assertEquals(list3.size(), 3);

Наконец, takeUntil() берет элементы из списка до тех пор, пока предикат не станет равен true. Существует вариант takeWhile(), который также принимает аргумент-предикат.

Кроме того, в API есть и другие полезные методы, например, даже distinct(), который возвращает список элементов с удаленными дубликатами, а также distinctBy(), который принимает Comparator для определения равенства.

Очень интересно, что также есть intersperse(), который вставляет элемент между каждым элементом списка. Это может быть очень удобно для операций со String:

List list8 = list
    .distinctBy((s1, s2) - > s1.startsWith(s2.charAt(0) + "") ? 0 : 1);
assertEquals(list3.size(), 2);

String words = List.of("Boys", "Girls")
    .intersperse("and")
    .reduce((s1, s2) - > s1.concat(" " + s2))
    .trim();
assertEquals(words, "Boys and Girls");

Хотите разделить список на категории? И для этого есть API:

Iterator < List < String >> iterator = list.grouped(2);
assertEquals(iterator.head().size(), 2);

Map < Boolean, List < String >> map = list.groupBy(e - > e.startsWith("J"));
assertEquals(map.size(), 2);
assertEquals(map.get(false).get().size(), 1);
assertEquals(map.get(true).get().size(), 5);

Метод group(int n) разделяет List на группы из n элементов каждая. Метод groupdBy() принимает Function, содержащую логику разделения списка и возвращает Map с двумя элементами: true и false.

Ключ true мапится на List элементов, удовлетворяющих условию, указанному в Function. Ключ false мапится на List элементов, не удовлетворяющих этому условию.

Как и ожидалось, при изменении List, исходный List на самом деле не изменяется. Вместо этого всегда возвращается новая версия List.

Мы также можем взаимодействовать с List, используя семантику стека — извлечение элементов по принципу «последним вошел, первым вышел» (LIFO). В этом смысле, для манипулирования стеком существуют такие методы API, как peek(), pop() и push():

List < Integer > intList = List.empty();

List < Integer > intList1 = intList.pushAll(List.rangeClosed(5, 10));

assertEquals(intList1.peek(), Integer.valueOf(10));

List intList2 = intList1.pop();
assertEquals(intList2.size(), (intList1.size() - 1));

Функция pushAll() используется для вставки диапазона целых чисел в стек, а функция peek() — для извлечения головного элемента стека. Существует также метод peekOption(), который может обернуть (wrap) результат в объект Option.

В интерфейсе List есть и другие интересные и действительно полезные методы, которые тщательно задокументированы в Java docs.

4.2. Queue


Неизменяемая (immutable) Queue хранит элементы, позволяя извлекать их по принципу FIFO (первым вошел, первым вышел).

Queue внутри состоит из двух связанных списков: переднего List и заднего List. Передний List содержит элементы, удаляемые из очереди, а задний List — элементы, поставленные в очередь.

Это позволяет привести операции постановки в очередь и удаления из очереди к сложности O(1). Когда при удалении из очереди в переднем List заканчиваются элементы, задний List реверсируется и становится новым передним List.

Давайте создадим очередь:

Queue < Integer > queue = Queue.of(1, 2);
Queue < Integer > secondQueue = queue.enqueueAll(List.of(4, 5));

assertEquals(3, queue.size());
assertEquals(5, secondQueue.size());

Tuple2 < Integer, Queue < Integer >> result = secondQueue.dequeue();
assertEquals(Integer.valueOf(1), result._1);

Queue < Integer > tailQueue = result._2;
assertFalse(tailQueue.contains(secondQueue.get(0)));

Функция dequeue() удаляет головной элемент из Queue и возвращает Tuple2<T, Q>. Первым элементом кортежа является головной элемент, удаленный из очереди, вторым элементом кортежа являются оставшиеся элементы Queue.

Мы можем использовать combination(n), чтобы получить все возможные N комбинаций элементов в Queue:

Queue < Queue < Integer >> queue1 = queue.combinations(2);
assertEquals(queue1.get(2).toCharSeq(), CharSeq.of("23"));

Снова видно, что исходная Queue не изменяется во время добавления/удаления элементов из очереди.

4.3. Stream


Stream — это реализация лениво-связанного списка, который значительно отличается от java.util.stream. В отличие от java.util.stream, Stream Vavr хранит данные и лениво вычисляет последующие элементы.
Допустим, у нас есть Stream целых чисел:

Stream < Integer > s = Stream.of(2, 1, 3, 4);

При печати результата s.toString() в консоли будет отображаться только Stream(2, ?). Это означает, что был вычислен только головной элемент Stream, в то время как хвостовые элементы нет.

Вызов s.get(3) и последующее отображение результата s.tail() возвращает Stream(1, 3, 4, ?). Напротив, если не вызвать первым s.get(3) — что заставит Stream вычислить последний элемент — результатом s.tail() будет только Stream(1, ?). Это означает, что был вычислен только первый элемент хвоста.

Такое поведение может улучшить производительность и позволяет использовать Stream для представления последовательностей, которые (теоретически) бесконечно длинны.
Stream в Vavr является неизменяемым и может быть Empty или Cons. Cons состоит из головного элемента и лениво вычисляемого хвоста Stream. В отличие от List, Stream хранит в памяти только головной элемент. Хвостовые элементы вычисляются по необходимости.

Давайте создадим Stream из 10 положительных целых чисел и вычислим сумму четных чисел:

Stream < Integer > intStream = Stream.iterate(0, i - > i + 1)
    .take(10);

assertEquals(10, intStream.size());

long evenSum = intStream.filter(i - > i % 2 == 0)
    .sum()
    .longValue();

assertEquals(20, evenSum);

В отличие от Stream API из Java 8, Stream в Vavr — это структура данных для хранения последовательности элементов.

Поэтому, у него есть такие методы, как get(), append(), insert() и другие для манипулирования его элементами. Также доступны drop(), distinct() и некоторые другие методы, рассмотренные ранее.

Наконец, давайте быстро продемонстрируем tabulate() в Stream. Этот метод возвращает Stream длиной n, содержащий элементы, являющиеся результатом применения функции:

Stream < Integer > s1 = Stream.tabulate(5, (i) - > i + 1);
assertEquals(s1.get(2).intValue(), 3);

Мы также можем использовать zip() для создания Stream из Tuple2<Integer, Integer>, который содержит элементы, образованные путем комбинирования двух Stream:

Stream < Integer > s = Stream.of(2, 1, 3, 4);

Stream < Tuple2 < Integer, Integer >> s2 = s.zip(List.of(7, 8, 9));
Tuple2 < Integer, Integer > t1 = s2.get(0);

assertEquals(t1._1().intValue(), 2);
assertEquals(t1._2().intValue(), 7);

4.4. Array


Array — это неизменяемая индексированная последовательность, обеспечивающая эффективный произвольный доступ. Он основан на Java массиве объектов. По сути, это Traversable оболочка для массива объектов типа T.

Можно создать экземпляр Array с помощью статического метода of(). Кроме того, можно создать диапазон элементов с помощью статических методов range() и rangeBy(). Метод rangeBy() имеет третий параметр, который позволяет определить шаг.

Методы range() и rangeBy() будут создавать элементы, начиная только с начального значения до конечного значения минус один. Если нам нужно включить конечное значение, можно использовать rangeClosed() или rangeClosedBy():

Array < Integer > rArray = Array.range(1, 5);
assertFalse(rArray.contains(5));

Array < Integer > rArray2 = Array.rangeClosed(1, 5);
assertTrue(rArray2.contains(5));

Array < Integer > rArray3 = Array.rangeClosedBy(1, 6, 2);
assertEquals(list3.size(), 3);

Давайте поработаем с элементами с использованием индекса:

Array < Integer > intArray = Array.of(1, 2, 3);
Array < Integer > newArray = intArray.removeAt(1);

assertEquals(3, intArray.size());
assertEquals(2, newArray.size());
assertEquals(3, newArray.get(1).intValue());

Array < Integer > array2 = intArray.replace(1, 5);
assertEquals(s1.get(0).intValue(), 5);

4.5. Vector


Vector — это что-то среднее между Array и List, предоставляющее другую индексированную последовательность элементов, позволяющую осуществлять и произвольный доступ, и модификацию за константное время:

Vector < Integer > intVector = Vector.range(1, 5);
Vector < Integer > newVector = intVector.replace(2, 6);

assertEquals(4, intVector.size());
assertEquals(4, newVector.size());

assertEquals(2, intVector.get(1).intValue());
assertEquals(6, newVector.get(1).intValue());

4.6. CharSeq


CharSeq — это объект-коллекция для представления последовательности примитивных символов. По сути — это оболочка для String с добавлением операций коллекции.

Чтобы создать CharSeq необходимо выполнить следующее.

CharSeq chars = CharSeq.of("vavr");
CharSeq newChars = chars.replace('v', 'V');

assertEquals(4, chars.size());
assertEquals(4, newChars.size());

assertEquals('v', chars.charAt(0));
assertEquals('V', newChars.charAt(0));
assertEquals("Vavr", newChars.mkString());

5. Set


В этом разделе рассматриваются различные реализации Set в библиотеке коллекций. Уникальная особенность структуры данных Set заключается в том, что она не допускает повторяющихся значений.

Существуют различные реализации Set. Основная из них — HashSet. TreeSet не допускает повторяющихся элементов и может быть отсортирован. LinkedHashSet сохраняет порядок вставки элементов.

Давайте рассмотрим более подробно эти реализации одну за другой.

5.1. HashSet


HashSet имеет статические фабричные методы для создания новых экземпляров. Некоторые из которых мы изучили ранее в этой статье, например of(), ofAll() и вариации методов range().

Разницу между двумя set можно получить с помощью метода diff(). Также, методы union() и intersect() возвращают объединение и пересечение двух set:

HashSet < Integer > set0 = HashSet.rangeClosed(1, 5);
HashSet < Integer > set0 = HashSet.rangeClosed(1, 5);

assertEquals(set0.union(set1), HashSet.rangeClosed(1, 6));
assertEquals(set0.diff(set1), HashSet.rangeClosed(1, 2));
assertEquals(set0.intersect(set1), HashSet.rangeClosed(3, 5));

Мы также можем выполнять основные операции, такие как добавление и удаление элементов:

HashSet < String > set = HashSet.of("Red", "Green", "Blue");
HashSet < String > newSet = set.add("Yellow");

assertEquals(3, set.size());
assertEquals(4, newSet.size());
assertTrue(newSet.contains("Yellow"));

Реализация HashSet основана на Hash array mapped trie (HAMT), которая может похвастаться превосходной производительностью по сравнению с обычной HashTable и ее структура делает ее подходящей для поддержки персистентных коллекций.

5.2. TreeSet


Неизменяемый TreeSet является реализацией интерфейса SortedSet. Он хранит набор отсортированных элементов и реализуется с применением бинарных деревьев поиска. Все его операции выполняются за время O(log n).

По умолчанию элементы TreeSet сортируются в их натуральном порядке.
Давайте создадим SortedSet используя натуральный порядок сортировки:

SortedSet < String > set = TreeSet.of("Red", "Green", "Blue");
assertEquals("Blue", set.head());

SortedSet < Integer > intSet = TreeSet.of(1, 2, 3);
assertEquals(2, intSet.average().get().intValue());

Чтобы упорядочить элементы кастомным образом, передайте экземпляр Comparator при создании TreeSet. Можно также создать строку из набора элементов:

SortedSet < String > reversedSet = TreeSet.of(Comparator.reverseOrder(), "Green", "Red", "Blue");
assertEquals("Red", reversedSet.head());

String str = reversedSet.mkString(" and ");
assertEquals("Red and Green and Blue", str);

5.3. BitSet


В коллекциях Vavr также присутствует неизменяемая реализация BitSet. Интерфейс BitSet расширяет интерфейс SortedSet. BitSet можно создать с помощью статических методов в BitSet.Builder.
Как и в других реализациях структуры данных Set, BitSet не позволяет добавлять в набор повторяющиеся записи.

Он наследует методы для манипулирования из интерфейса Traversable. Обратите внимание, что он отличается от java.util.BitSet из стандартной библиотеки Java. Данные BitSet не могут содержать значения String.

Рассмотрим создание экземпляра BitSet с использованием фабричного метода of():

BitSet < Integer > bitSet = BitSet.of(1, 2, 3, 4, 5, 6, 7, 8);
BitSet < Integer > bitSet1 = bitSet.takeUntil(i - > i > 4);
assertEquals(list3.size(), 4);

Для выбора первых четырех элементов BitSet мы использовали команду takeUntil(). Операция вернула новый экземпляр. Обратите внимание, что метод takeUntil() определен в интерфейсе Traversable, который является родительским интерфейсом для BitSet.

Другие методы и операции, описанные выше, определенные в интерфейсе Traversable, также применимы к BitSet.

6. Map


Map — это структура данных «ключ-значение». Map в Vavr является неизменяемой и имеет реализации для HashMap, TreeMap и LinkedHashMap.

Как правило, контракты map не допускают дублирование ключей, в то время как повторяющиеся значения, сопоставленные с различными ключами, могут быть.

6.1. HashMap


HashMap — это реализация неизменяемого интерфейса Map. Он хранит пары «ключ-значение», используя хэш-код ключей.

Map в Vavr использует Tuple2 для представления пар «ключ-значение» вместо традиционного типа Entry:

Map < Integer, List < Integer >> map = List.rangeClosed(0, 10)
    .groupBy(i - > i % 2);

assertEquals(2, map.size());
assertEquals(6, map.get(0).get().size());
assertEquals(5, map.get(1).get().size());

Как и HashSet, реализация HashMap основана на Hash array mapped trie (HAMT), что приводит к константному времени почти для всех операций.
Элементы map можно фильтровать по ключам с помощью метода filterKeys() или по значениям с помощью метода filterValues(). Оба метода принимают Predicate в качестве аргумента:

Map < String, String > map1 = 
    HashMap.of("key1", "val1", "key2", "val2", "key3", "val3");

Map < String, String > fMap = 
    map1.filterKeys(k - > k.contains("1") || k.contains("2"));
assertFalse(fMap.containsKey("key3"));

Map < String, String > map1 = 
    map1.filterValues(v - > v.contains("3"));
assertEquals(list3.size(), 1);
assertTrue(fMap2.containsValue("val3"));

Также можно преобразовать элементы map с помощью метода map(). Давайте, например, преобразуем map1 в Map<String, Integer>:

Map < String, Integer > map2 = map1.map(
    (k, v) - > Tuple.of(k, Integer.valueOf(v.charAt(v.length() - 1) + "")));
assertEquals(map2.get("key1").get().intValue(), 1);

6.2. TreeMap


Неизменяемая TreeMap является реализацией интерфейса SortedMap. Как и в случае TreeSet, для кастомной сортировки элементов TreeMap используется экземпляр Comparator.
Продемонстрируем создание SortedMap:

SortedMap < Integer, String > map = TreeMap.of(3, "Three", 2, "Two", 4, "Four", 1, "One");

assertEquals(1, map.keySet().toJavaArray()[0]);
assertEquals("Four", map.get(4).get());

По умолчанию, записи TreeMap сортируются в натуральном порядке ключей. Однако, можно указать Comparator, который будет использоваться для сортировки:

TreeMap < Integer, String > treeMap2 =
    TreeMap.of(Comparator.reverseOrder(), 3, "three", 6, "six", 1, "one");
assertEquals(treeMap2.keySet().mkString(), "631");

Как и в случае TreeSet, реализация TreeMap также создана с применением дерева, следовательно, его операции имеют время O(log n). Метод map.get(key) возвращает Option, который содержит значение указанного ключа map.

7. Совместимость с Java


API коллекций Vavr полностью совместим с фреймворком коллекций Java. Посмотрим, как это делается на практике.

7.1. Преобразование из Java в Vavr


Каждая реализация коллекции в Vavr имеет статический фабричный метод ofAll(), который принимает java.util.Iterable. Это позволяет создать коллекцию Vavr из коллекции Java. Аналогично, другой фабричный метод ofAll() принимает непосредственно Java Stream.

Чтобы преобразовать List Java в неизменяемый List :

java.util.List < Integer > javaList = java.util.Arrays.asList(1, 2, 3, 4);
List < Integer > vavrList = List.ofAll(javaList);

java.util.stream.Stream < Integer > javaStream = javaList.stream();
Set < Integer > vavrSet = HashSet.ofAll(javaStream);

Другой полезной функцией является collector(), который можно использовать совместно с Stream.collect() для получения коллекции Vavr:

List < Integer > vavrList = IntStream.range(1, 10)
    .boxed()
    .filter(i - > i % 2 == 0)
    .collect(List.collector());

assertEquals(4, vavrList.size());
assertEquals(2, vavrList.head().intValue());

7.2. Преобразование из Vavr в Java


Интерфейс Value имеет множество методов для преобразования из типа Vavr в тип Java. Эти методы имеют формат toJavaXXX().

Рассмотрим пару примеров:

Integer[] array = List.of(1, 2, 3)
    .toJavaArray(Integer.class);
assertEquals(3, array.length);

java.util.Map < String, Integer > map = List.of("1", "2", "3")
    .toJavaMap(i - > Tuple.of(i, Integer.valueOf(i)));
assertEquals(2, map.get("2").intValue());

Также мы можем использовать Java 8 Collectors для сбора элементов из коллекций Vavr:

java.util.Set < Integer > javaSet = List.of(1, 2, 3)
    .collect(Collectors.toSet());

assertEquals(3, javaSet.size());
assertEquals(1, javaSet.toArray()[0]);

7.3. Представления Коллекций Java


Кроме того, библиотека предоставляет так называемые представления коллекций, которые работают лучше при преобразовании в коллекции Java. Методы преобразования, приведенные в предыдущем разделе, перебирают (итерируют) все элементы для создания коллекции Java.

Представления, с другой стороны, реализуют стандартные интерфейсы Java и делегируют вызовы методов базовой коллекции Vavr.

На момент написания этой статьи поддерживается только представление List. Каждая последовательная коллекция имеет два метода: один для создания неизменяемого представления, другой для изменяемого.

Вызов методов для изменения на неизменяемом представлении приводит к исключению UnsupportedOperationException.

Давайте рассмотрим пример:

@Test(expected = UnsupportedOperationException.class)
public void givenVavrList_whenViewConverted_thenException() {
    java.util.List < Integer > javaList = List.of(1, 2, 3)
        .asJava();

    assertEquals(3, javaList.get(2).intValue());
    javaList.add(4);
}

Чтобы создать неизменяемое представление:

java.util.List < Integer > javaList = List.of(1, 2, 3)
    .asJavaMutable();
javaList.add(4);

assertEquals(4, javaList.get(3).intValue());

8. Выводы


В этом уроке мы узнали о различных функциональных структурах данных, предоставляемых API Коллекций Vavr. Есть еще полезные и производительные методы API, которые можно найти в Java doc и руководстве пользователя коллекций Vavr.

Наконец, важно отметить, что библиотека также определяет Try, Option, Either и Future, которые расширяют интерфейс Value и, как следствие, реализуют интерфейс Java Iterable. Это означает, что в некоторых ситуациях они могут вести себя, как коллекции.

Полный исходный код для всех примеров в этой статье можно найти на Github.

Дополнительные материалы:
habr.com/ru/post/421839
www.baeldung.com/vavr

Перевел @middle_java
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы используете Vavr в разработке?
16.39% Да10
57.38% Нет35
26.23% Что это?16
Проголосовал 61 пользователь. Воздержались 10 пользователей.
Теги:
Хабы:
Всего голосов 10: ↑8 и ↓2+6
Комментарии0

Публикации

Истории

Работа

Java разработчик
443 вакансии

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань