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

Source: https://habr.com/ru/post/474402/


All Articles