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

Руководство по коллекциям API в Вавре

1. обзор

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

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

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

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

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

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

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

3. Traversableс

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

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

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

4. Seqс

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

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

4.1. Listс

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

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

  • Голова - первый элемент

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

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

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

List 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() продолжает отбрасывать элементы, пока предикат истинен.

Также есть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 есть и другие полезные методы, например, фактическиdistinct(), который возвращает список недублирующихся элементов, а такжеdistinctBy(), который принимаетComparator для определения равенства.

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

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> iterator = list.grouped(2);
assertEquals(iterator.head().size(), 2);

Map> 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 intList = List.empty();

List 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 docs.

4.2. Queueс

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

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

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

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

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

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

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

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

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

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

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

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

4.3. Streamс

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4.4. Arrayс

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

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

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

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

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

Array rArray3 = Array.rangeClosedBy(1,6,2);
assertEquals(rArray3.size(), 3);

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

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

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

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

4.5. Vectorс

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

Vector intVector = Vector.range(1, 5);
Vector 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().

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

HashSet set0 = HashSet.rangeClosed(1,5);
HashSet 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 set = HashSet.of("Red", "Green", "Blue");
HashSet 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. Он хранитSet отсортированных элементов и реализуется с использованием двоичных деревьев поиска. Все его операции выполняются за O (log n) времени.

По умолчанию элементы aTreeSet отсортированы в их естественном порядке.

Давайте создадимSortedSet, используя естественный порядок сортировки:

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

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

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

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

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

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

6. Mapс

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

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

6.1. HashMapс

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

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

Map> 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 map1
  = HashMap.of("key1", "val1", "key2", "val2", "key3", "val3");

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

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

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

Map 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, экземплярComparator используется для настраиваемых элементов сортировкиTreeMap.

Продемонстрируем созданиеSortedMap:

SortedMap 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 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 имеет статический фабричный методofAll(), который принимаетjava.util.Iterable. Это позволяет нам создавать коллекцию Vavr из коллекции Java. Точно так же другой фабричный методofAll() напрямую принимает JavaStream.

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

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

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

Еще одна полезная функция -collector(), которую можно использовать вместе сStream.collect() для получения коллекции Vavr:

List 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 map = List.of("1", "2", "3")
  .toJavaMap(i -> Tuple.of(i, Integer.valueOf(i)));
assertEquals(2, map.get("2").intValue());

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

java.util.Set 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 javaList = List.of(1, 2, 3)
      .asJava();

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

Чтобы создать неизменный вид:

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

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

8. Заключение

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

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

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