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 .