Vavr Collections API Guide

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_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 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



All Articles