Under the cut is a translation of an article systematizing information on the Vavr Collections API .
Translated by @middle_java
Last Modified Original Article: August 15, 2019
1. Overview
The Vavr library, formerly known as Javaslang, is a functional library for Java. In this article, we explore its powerful collection API.
For more information about this library, see this article .
2. Persistent Collections
A persistent collection, when modified, creates a new version of the collection without changing the current version.
Support for multiple versions of the same collection can lead to inefficient use of CPU and memory. However, the Vavr collection library overcomes this by sharing the data structure between different versions of the collection.
This is fundamentally different from
unmodifiableCollection()
from the Java utility
Collections
class, which simply provides a wrapper for the base collection.
Attempting to modify such a collection
UnsupportedOperationException
instead of creating a new version. Moreover, the base collection is still mutable through a direct link to it.
3. Traversable
Traversable
is the base type of all Vavr collections. This interface defines methods common to all data structures.
It provides some useful default methods, such as
size()
,
get()
,
filter()
,
isEmpty()
and others that are inherited by sub-interfaces.
We further explore the collection library.
4. Seq
Let's start with the sequences.
The
Seq
interface is a sequential data structure. This is the parent interface for
List
,
Stream
,
Queue
,
Array
,
Vector
and
CharSeq
. All these data structures have their own unique properties, which we will discuss below.
4.1. List
List
is an energetically calculated (eagerly-evaluated, operation is performed as soon as the values โโof its operands become known) a sequence of elements that extend the
LinearSeq
interface.
Persistent
List
constructed recursively using the head and tail :
- The head is the first element
- Tail - a list containing the remaining elements (this list is also formed from the head and tail)
The
List
API contains static factory methods that you can use to create a
List
. You can use the static
of()
method to create an instance of
List
from one or more objects.
You can also use the static
empty()
method to create an empty
List
and the
ofAll()
method to create a
List
of type
Iterable
:
List < String > list = List.of( "Java", "PHP", "Jquery", "JavaScript", "JShell", "JAVA");
Consider some examples of list manipulation.
We can use the
drop()
method and its variants to remove the first
N
elements:
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)
removes
n
items from the list, starting from the first item, while
dropRight()
does the same, starting from the last item in the list.
dropUntil()
removes the items from the list until the predicate is
true
, while
dropWhile()
removes the items until the predicate is
true
.
There are also
dropRightWhile()
and
dropRightUntil()
methods that remove items starting from the right.
Next,
take(int n)
used to retrieve items from the list. It takes
n
items from the list and then stops. There is also
takeRight(int n)
, which takes elements from the end of the list:
List list5 = list.take(1); assertEquals(list5.single(), "Java"); List list6 = list.takeRight(1); assertEquals(list5.single(), "Java"); List list7 = list.takeUntil(s - > s.length() > 6); assertEquals(list3.size(), 3);
Finally,
takeUntil()
takes elements from the list until the predicate becomes
true
. There is a
takeWhile()
option that also takes a predicate argument.
In addition, the API has other useful methods, for example, even
distinct()
, which returns a list of elements with deleted duplicates, as well as
distinctBy()
, which accepts
Comparator
to determine equality.
It is very interesting that there is also
intersperse()
, which inserts an element between each element of the list. This can be very convenient for
String
operations:
List list8 = list .distinctBy((s1, s2) - > s1.startsWith(s2.charAt(0) + "") ? 0 : 1); assertEquals(list3.size(), 2); String words = List.of("Boys", "Girls") .intersperse("and") .reduce((s1, s2) - > s1.concat(" " + s2)) .trim(); assertEquals(words, "Boys and Girls");
Want to split the list into categories? And there is an API for this:
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);
The
group(int n)
method splits
List
into groups of
n
elements each. The
groupdBy()
method accepts a
Function
that contains the list splitting logic and returns a
Map
with two elements:
true
and
false
.
The
true
key is mapped onto the
List
elements satisfying the condition specified in
Function
. The
false
key maps to the
List
elements that do not satisfy this condition.
As expected, when changing the
List
, the original
List
is not really changing. Instead, the new version of
List
always returned.
We can also interact with
List
using the semantics of the stack - extracting elements according to the โlast in, first outโ principle (LIFO). In this sense, there are API methods for manipulating the stack such as
peek()
,
pop()
and
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));
The
pushAll()
function is used to insert a range of integers onto the stack, and the
peek()
function is used to retrieve the head element of the stack. There is also a
peekOption()
method that can wrap the result in an
Option
object.
There are other interesting and really useful methods in the
List
interface that are thoroughly documented in Java docs .
4.2. Queue
The immutable
Queue
stores elements, allowing you to retrieve them according to the FIFO principle (first in, first out).
Queue
inside consists of two linked lists: the front
List
and the back
List
. The front
List
contains items that are removed from the queue, and the rear
List
contains the items queued.
This allows you to put the operations of queuing and removing from the queue to complexity O (1) . When the
List
ends in the front
List
when it is removed from the queue, the back
List
reversed and becomes the new front
List
.
Let's create a queue:
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)));
The
dequeue()
function removes the head element from
Queue
and returns
Tuple2<T, Q>
. The first element of the tuple is the head element removed from the queue, the second element of the tuple is the remaining
Queue
elements.
We can use
combination(n)
to get all possible
N
combinations of elements in
Queue
:
Queue < Queue < Integer >> queue1 = queue.combinations(2); assertEquals(queue1.get(2).toCharSeq(), CharSeq.of("23"));
Once again, the original
Queue
does not change while adding / removing items from the queue.
4.3. Stream
Stream
is an implementation of a lazily linked list that is significantly different from
java.util.stream
. Unlike
java.util.stream
,
Stream
Vavr stores data and lazily computes subsequent elements.
Let's say we have
Stream
integers:
Stream < Integer > s = Stream.of(2, 1, 3, 4);
When printing the result of
s.toString()
in the console, only Stream (2,?) Will be displayed. This means that only the
Stream
head element was calculated, while the tail elements were not.
Calling
s.get(3)
and then displaying the result of
s.tail()
returns Stream (1, 3, 4,?) . On the contrary, if you do not call
s.get(3)
- which makes
Stream
calculate the last element - only Stream (1,?) Will be the result of
s.tail()
) . This means that only the first tail element was calculated.
This behavior can improve performance and allows
Stream
to be used to represent sequences that are (theoretically) infinitely long.
Stream
in Vavr is immutable and can be
Empty
or
Cons
.
Cons
consists of the head element and the lazily calculated tail of the
Stream
. Unlike
List
,
Stream
stores the head element in memory. Tail elements are calculated as needed.
Let's create a
Stream
of 10 positive integers and calculate the sum of even numbers:
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);
Unlike the
Stream
API from Java 8,
Stream
in Vavr is a data structure for storing a sequence of elements.
Therefore, it has methods such as
get()
,
append()
,
insert()
and others for manipulating its elements.
drop()
,
distinct()
and some other methods discussed earlier are also available.
Finally, let's quickly demonstrate
tabulate()
in
Stream
. This method returns a
Stream
length
n
containing elements that are the result of applying the function:
Stream < Integer > s1 = Stream.tabulate(5, (i) - > i + 1); assertEquals(s1.get(2).intValue(), 3);
We can also use
zip()
to create a
Stream
from
Tuple2<Integer, Integer>
, which contains elements formed by combining two
Stream
:
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. Array
Array
is an immutable indexed sequence that provides efficient random access. It is based on a Java array of objects. In essence, this is a
Traversable
wrapper for an array of objects of type
T
You can create an
Array
instance using the
of()
static method. In addition, you can create a range of elements using the static methods
range()
and
rangeBy()
. The
rangeBy()
method has a third parameter, which allows you to determine the step.
The
range()
and
rangeBy()
methods will create elements, starting only from the initial value to the final value minus one. If we need to include the final value, we can use
rangeClosed()
or
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(list3.size(), 3);
Let's work with the elements using the index:
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(s1.get(0).intValue(), 5);
4.5. Vector
Vector
is a cross between
Array
and
List
, providing another indexed sequence of elements, allowing both random access and modification in constant time:
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
is a collection object for representing a sequence of primitive characters. In essence, it is a wrapper for
String
with the addition of collection operations.
To create
CharSeq
you must do the following.
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
This section discusses the various implementations of
Set
in the collection library. A unique feature of the
Set
data structure is that it does not allow duplicate values.
There are various implementations of
Set
. The main one is
HashSet
.
TreeSet
does not allow duplicate elements and can be sorted.
LinkedHashSet
preserves the insertion order of elements.
Let's take a closer look at these implementations one after another.
5.1. Hashset
HashSet
has static factory methods for creating new instances. Some of which we studied earlier in this article, for example
of()
,
ofAll()
and variations on the
range()
methods.
The difference between the two set can be obtained using the
diff()
method. Also, the
union()
and
intersect()
methods return the union and intersection of two set :
HashSet < Integer > set0 = HashSet.rangeClosed(1, 5); HashSet < Integer > set0 = HashSet.rangeClosed(1, 5); assertEquals(set0.union(set1), HashSet.rangeClosed(1, 6)); assertEquals(set0.diff(set1), HashSet.rangeClosed(1, 2)); assertEquals(set0.intersect(set1), HashSet.rangeClosed(3, 5));
We can also perform basic operations, such as adding and removing elements:
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"));
The
HashSet
implementation is based on the Hash array mapped trie (HAMT) , which boasts superior performance compared to the regular
HashTable
and its structure makes it suitable for supporting persistent collections.
5.2. Treeset
Immutable
TreeSet
is an implementation of the
SortedSet
interface. It stores a set of sorted elements and is implemented using binary search trees. All its operations are performed during O (log n) time .
By default,
TreeSet
elements are sorted in their natural order.
Let's create a
SortedSet
using a natural sort order:
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());
To arrange items in a custom way, pass a
Comparator
instance when creating the
TreeSet
. You can also create a string from a set of elements:
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 collections also have an immutable
BitSet
implementation. The
BitSet
interface extends the
SortedSet
interface.
BitSet
can be created using static methods in
BitSet.Builder
.
As with other implementations of the
Set
data structure,
BitSet
does not allow you to add duplicate records to a set.
It inherits methods for manipulation from the
Traversable
interface. Note that it is different from
java.util.BitSet
from the standard Java library.
BitSet
data cannot contain
String
values.
Consider creating an instance of
BitSet
using the
of()
factory method:
BitSet < Integer > bitSet = BitSet.of(1, 2, 3, 4, 5, 6, 7, 8); BitSet < Integer > bitSet1 = bitSet.takeUntil(i - > i > 4); assertEquals(list3.size(), 4);
To select the first four
BitSet
elements
BitSet
we used the
takeUntil()
command. The operation returned a new instance. Note that the
takeUntil()
method is defined in the
Traversable
interface, which is the parent interface for
BitSet
.
Other methods and operations described above defined in the
Traversable
interface also apply to
BitSet
.
6. Map
Map
is a key-value data structure.
Map
in Vavr is immutable and has implementations for
HashMap
,
TreeMap
and
LinkedHashMap
.
Typically, map contracts do not allow duplicate keys, while duplicate values โโmapped to different keys can be.
6.1. Hashmap
HashMap
is an implementation of the immutable
Map
interface. It stores key-value pairs using a hash of keys.
Map
in Vavr uses
Tuple2
to represent key-value pairs instead of the traditional
Entry
type:
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());
Like
HashSet
, the implementation of
HashMap
based on the Hash array mapped trie (HAMT) , which leads to constant time for almost all operations.
Map elements can be filtered by key using the
filterKeys()
method or by value using the
filterValues()
method. Both methods take
Predicate
as an argument:
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 > map1 = map1.filterValues(v - > v.contains("3")); assertEquals(list3.size(), 1); assertTrue(fMap2.containsValue("val3"));
You can also transform map elements using the
map()
method. For example, let's convert map1 to
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
Immutable
TreeMap
is an implementation of the
SortedMap
interface. As with
TreeSet
, a custom instance of
Comparator
used to customize the sorting of
TreeMap
elements.
SortedMap
demonstrate the creation of
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());
By default,
TreeMap
entries are sorted in natural key order. However, you can specify the
Comparator
to be used for sorting:
TreeMap < Integer, String > treeMap2 = TreeMap.of(Comparator.reverseOrder(), 3, "three", 6, "six", 1, "one"); assertEquals(treeMap2.keySet().mkString(), "631");
As in the case of
TreeSet
, the implementation of
TreeMap
also created using the tree, therefore, its operations have time O (log n) . The
map.get(key)
method returns
Option
, which contains the value of the specified map key.
7. Java compatibility
The Vavr Collection API is fully compatible with the Java Collection Framework. Let's see how this is done in practice.
7.1. Convert from Java to Vavr
Each collection implementation in Vavr has a static factory
ofAll()
method, which accepts
java.util.Iterable
. This allows you to create a Vavr collection from a Java collection. Similarly, another
ofAll()
factory method directly accepts Java
Stream
.
To convert a Java
List
to an immutable
List
:
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);
Another useful function is
collector()
, which can be used in conjunction with
Stream.collect()
to get the Vavr collection:
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. Convert from Vavr to Java
The
Value
interface has many methods for converting from a Vavr type to a Java type. These methods have the format
toJavaXXX()
.
Consider a couple of examples:
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());
We can also use Java 8
Collectors
to collect items from Vavr collections:
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 Collections Views
In addition, the library provides so-called collection views that work best when converted to Java collections. The transformation methods in the previous section iterate over (iterate) all the elements to create a Java collection.
Views, on the other hand, implement standard Java interfaces and delegate method calls to the Vavr base collection.
At the time of this writing, only the
List
view is supported. Each sequential collection has two methods: one for creating an immutable representation, the other for mutable.
Calling methods to change on an immutable view
UnsupportedOperationException
.
Let's look at an example:
@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); }
To create an immutable view:
java.util.List < Integer > javaList = List.of(1, 2, 3) .asJavaMutable(); javaList.add(4); assertEquals(4, javaList.get(3).intValue());
8. Conclusions
In this tutorial, we learned about the various functional data structures provided by the Vavr Collections API. There are also useful and productive API methods that can be found in the Java doc and the Vavr collections user guide.
Finally, it is important to note that the library also defines
Try
,
Option
,
Either
and
Future
, which extend the
Value
interface and, as a result, implement the Java
Iterable
interface. This means that in some situations they can behave like collections.
The full source code for all the examples in this article can be found on Github .
Additional materials:
habr.com/en/post/421839
www.baeldung.com/vavr
Translated by @middle_java