Рубрики
Без рубрики

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

Откройте для себя мощный API постоянных коллекций Vavr.

Автор оригинала: baeldung.

1. Обзор

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

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

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

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

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

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

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

3. Проходимость

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

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

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

4. Seq

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

Интерфейс Seq представляет последовательные структуры данных. Это родительский интерфейс для Списка , Потока , Очереди , Массива , Вектора и CharSeq . Все эти структуры данных обладают своими уникальными свойствами, которые мы рассмотрим ниже.

4.1. Список

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

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

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

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

Мы также можем использовать статический empty() для создания пустого списка и всех() для создания Списка из Итеративного типа:

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 количество элементов из списка, начиная с первого элемента, в то время как drop Right() делает то же самое, начиная с последнего элемента в списке.

drop Until() продолжает удалять элементы из списка до тех пор, пока предикат не примет значение true, тогда как drop While() продолжает удалять элементы, пока предикат не станет истинным.

Есть также dropRightWhile() и drop Right Until () , который начинает удалять элементы справа.

Далее, 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);

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

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

Очень интересно, что есть также 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) делит Список на группы n элементов в каждой. groupdBy() принимает Функцию , содержащую логику разделения списка, и возвращает Map с двумя записями – true и false .

Ключ true сопоставляется со списком элементов, удовлетворяющих условию, указанному в функции ; ключ false сопоставляется со списком элементов, которые этого не делают.

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

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

4.2. Очередь

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

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

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

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

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 удаляет элемент head из Очереди и возвращает Tuple2 Q> . Кортеж содержит элемент head, который был удален в качестве первой записи, и оставшиеся элементы Очереди в качестве второй записи. Q>

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

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

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

4.3. Поток

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

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

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

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

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

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

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

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

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

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

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

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

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

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. Массив

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

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

Методы range() и range By() будут генерировать только элементы, начиная с начального значения до конечного значения минус единица. Если нам нужно включить конечное значение, мы можем использовать либо range Closed () , либо 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. Вектор

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

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 с добавлением операций сбора.

Чтобы создать Char Seq :

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

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

5.1. Хэш-набор

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. Набор деревьев

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

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

Давайте создадим 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. Набор битов

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

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

Он наследует методы для манипуляций из проходимого интерфейса. Обратите внимание, что он отличается от 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() определен в Проходимом интерфейсе, который является родительским интерфейсом BitSet.

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

6. Карта

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

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

6.1. Хэш-карта

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

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

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

Мы можем фильтровать записи карты по ключам, используя метод filter Keys () , или по значениям, используя метод filter Values () . Оба метода принимают Предикат в качестве аргумента:

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 Integer> : 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. Древовидная карта

Неизменяемая Древовидная карта является реализацией интерфейса 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 сортируются в естественном порядке ключей. Однако мы можем указать Компаратор , который будет использоваться для сортировки:

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

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

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

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

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

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

Чтобы преобразовать Java List в неизменяемый 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() для получения обширной коллекции:

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. Преобразование Var в Java

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

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

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 8 Collectors для сбора элементов из коллекций Var:

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 и делегируют вызовы методов базовой коллекции Var.

На момент написания этой статьи поддерживается только представление 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. Заключение

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

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

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