«1. Обзор

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

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

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

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

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

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

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

3. Traversable

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

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

Давайте поближе познакомимся с библиотекой коллекций.

4. Seq

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

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

4.1. Список

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

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

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

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

Есть также функции 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);

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

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

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

Мы также можем взаимодействовать со списком, используя семантику стека — извлечение элементов в порядке поступления (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)));

Функция dequeue удаляет из очереди элемент head и возвращает Tuple2\u003cT, Q\u003e. Кортеж содержит удаленный элемент head в качестве первой записи и оставшиеся элементы Queue в качестве второй записи.

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

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

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

4.3. Stream

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

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

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

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

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

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

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

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

5.1. HashSet

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

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 не могут содержать строковые значения.

Давайте посмотрим, как создать экземпляр 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 Map использует 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\u003cString, Integer\u003e:

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, экземпляр 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);

Другая полезная функция — collect(), которую можно использовать вместе с 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. API. Есть более полезные и продуктивные методы API, которые можно найти в сборниках Vavr JavaDoc и руководстве пользователя.

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

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