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

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

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

1. Обзор

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

Чтобы получить больше информации об этой библиотеке, пожалуйста, прочитайте эту статью .

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

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

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

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

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

3. Проходной

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

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

Давайте изучим библиотеку коллекций дальше.

4. След .

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

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

4.1. Список

Список — это тщательно оцененная последовательность элементов, расширяющая интерфейс LinearSeq . ``

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

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

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

Мы также можем использовать static empty() для создания пустого списка и ofAll() для создания списка из типа 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() продолжает удалять элементы из списка до тех пор, пока предикат не станет истинным, тогда как 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(list6.single(), "JAVA");

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

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

Более того, в API есть и другие полезные методы, например, Different() , который возвращает список неповторяющихся элементов, а также DifferentBy() , который принимает Компаратор для определения равенства.

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

List list8 = list
.distinctBy((s1, s2) -> s1.startsWith(s2.charAt(0) + "") ? 0 : 1);
assertEquals(list8.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);

Группа (int n) делит список на группы по n элементов в каждой. groupdBy () принимает функцию , которая содержит логику для разделения списка, и возвращает карту с двумя записями — true и false .

Истинный ключ сопоставляется со списком элементов , которые удовлетворяют условию, указанному в функции; ложный ключ сопоставляется со списком элементов, которые этого не делают.

Как и ожидалось, при изменении 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() , которая может обернуть результат в объект Option .

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

4.2. Очередь

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

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

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

Создадим очередь:

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)));

Функция удаления из очереди удаляет элемент заголовка из очереди и возвращает Tuple2 <T, Q> . Кортеж содержит удаленный элемент head в качестве первой записи и оставшиеся элементы Queue в качестве второй записи. ``

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

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

Опять же, мы видим, что исходная очередь не изменяется при постановке/удалении элементов из очереди .

4.3. Ручей

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

Допустим, у нас есть поток целых чисел:

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

Вывод результата s.toString() на консоль покажет только Stream(2, ?) . Это означает, что оценивается только начало потока , а хвост не оценивается.

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

Такое поведение может повысить производительность и позволяет использовать Stream для представления последовательностей, которые (теоретически) бесконечно длинны.

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

Давайте создадим поток из 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);

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

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

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

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

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

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. Множество

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

Мы можем создать экземпляр массива , используя статический метод () . Мы также можем сгенерировать элементы диапазона, используя методы static 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(rArray3.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(array2.get(0).intValue(), 5);

4.5. Вектор

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

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 , основной из которых является HashSet . TreeSet не допускает дублирования элементов и может быть отсортирован . LinkedHashSet поддерживает порядок вставки своих элементов.

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

5.1. Хэшсет

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

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

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

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 опирается на сопоставленное дерево хэш-массива (HAMT) , которое может похвастаться превосходной производительностью по сравнению с обычной HashTable , а его структура делает его пригодным для поддержки постоянной коллекции.

5.2. Набор деревьев

Неизменяемый 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. Битсет

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

Как и другие реализации структуры данных Set , BitSet не позволяет добавлять в набор повторяющиеся записи.

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

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

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

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

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

6. Карта

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

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

6.1. HashMap

HashMap — это реализация неизменяемого интерфейса 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 поддерживается хеш-массивом сопоставленного дерева (HAMT), что приводит к постоянному времени почти для всех операций.

Мы можем фильтровать записи карты по ключам, используя метод 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> fMap2
= map1.filterValues(v -> v.contains("3"));
assertEquals(fMap2.size(), 1);
assertTrue(fMap2.containsValue("val3"));

Мы также можем преобразовать записи карты, используя метод 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 — это реализация интерфейса SortedMap . Подобно TreeSet , экземпляр Comparator используется для пользовательской сортировки элементов TreeMap .

Давайте продемонстрируем создание 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 , который упаковывает значение в указанный ключ на карте.

7. Взаимодействие с Java

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

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

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

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

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);

Еще одна полезная функция — collection( ) , которую можно использовать вместе с 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 для сбора элементов из коллекций 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.

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

Вызов методов-мутаторов для неизменяемого представления приводит к 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. Заключение

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

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

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