VAVR (formerly known as Javaslang) is a non-profit functional library for Java 8+. It allows you to write functional Scala-like code in Java and serves to reduce the amount of code and improve its quality.
Library site .
Under the cut is a translation of an article systematizing information on
the Vavr Collections API .
Translated by
@middle_javaLast 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
TYou 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/421839www.baeldung.com/vavrTranslated by
@middle_java