1. Overview
In this tutorial, we’ll look at the major features of Protonpack which is a library that expands the standard Stream API by adding some complimentary functionality.
Refer to this writeup here to discover the fundamentals of the Java Stream API.
2. Maven Dependency
To use the Protonpack library, we need to add a dependency in our pom.xml file:
<dependency> <groupId>com.codepoetics</groupId> <artifactId>protonpack</artifactId> <version>1.15</version> </dependency>
Check for the latest version on Maven Central.
3. StreamUtils
This is the main class that expands Java’s standard Stream API.
All the methods discussed here are intermediate operations, which means they modify a Stream but doesn’t trigger its processing.
3.1. takeWhile() and takeUntil()
takeWhile() takes values from the source stream as long as they meet the supplied condition:
Stream<Integer> streamOfInt = Stream .iterate(1, i -> i + 1); List<Integer> result = StreamUtils .takeWhile(streamOfInt, i -> i < 5) .collect(Collectors.toList()); assertThat(result).contains(1, 2, 3, 4);
Conversely, takeUntil() takes values until a value meets supplied condition and then stops:
Stream<Integer> streamOfInt = Stream .iterate(1, i -> i + 1); List<Integer> result = StreamUtils .takeUntil(streamOfInt, i -> i >= 5) .collect(Collectors.toList()); assertThat(result).containsExactly(1, 2, 3, 4);
In Java 9 onward, takeWhile() is part of the standard Stream API.
3.2. zip()
zip() takes two or three streams as an input and a combiner function. The method takes a value from the same position of each stream and passes it to the combiner.
It does so until one of the streams runs out of values:
String[] clubs = {"Juventus", "Barcelona", "Liverpool", "PSG"}; String[] players = {"Ronaldo", "Messi", "Salah"}; Set<String> zippedFrom2Sources = StreamUtils .zip(stream(clubs), stream(players), (club, player) -> club + " " + player) .collect(Collectors.toSet()); assertThat(zippedFrom2Sources) .contains("Juventus Ronaldo", "Barcelona Messi", "Liverpool Salah");
Similarly, an overloaded zip() that takes three sources stream:
String[] leagues = { "Serie A", "La Liga", "Premier League" }; Set<String> zippedFrom3Sources = StreamUtils .zip(stream(clubs), stream(players), stream(leagues), (club, player, league) -> club + " " + player + " " + league) .collect(Collectors.toSet()); assertThat(zippedFrom3Sources).contains( "Juventus Ronaldo Serie A", "Barcelona Messi La Liga", "Liverpool Salah Premier League");
3.3. zipWithIndex()
zipWithIndex() takes values and zip each value with its index to create a stream of indexed values:
Stream<String> streamOfClubs = Stream .of("Juventus", "Barcelona", "Liverpool"); Set<Indexed<String>> zipsWithIndex = StreamUtils .zipWithIndex(streamOfClubs) .collect(Collectors.toSet()); assertThat(zipsWithIndex) .contains(Indexed.index(0, "Juventus"), Indexed.index(1, "Barcelona"), Indexed.index(2, "Liverpool"));
3.4. merge()
merge() works with multiple source streams and a combiner. It takes the value of the same index position from each source stream and passes it to the combiner.
The method works by taking 1 value from the same index from each stream in succession, starting from the seed value.
Then the value is passed to the combiner, and the resulting combined value is fed back to the combiner to create the next value:
Stream<String> streamOfClubs = Stream .of("Juventus", "Barcelona", "Liverpool", "PSG"); Stream<String> streamOfPlayers = Stream .of("Ronaldo", "Messi", "Salah"); Stream<String> streamOfLeagues = Stream .of("Serie A", "La Liga", "Premier League"); Set<String> merged = StreamUtils.merge( () -> "", (valOne, valTwo) -> valOne + " " + valTwo, streamOfClubs, streamOfPlayers, streamOfLeagues) .collect(Collectors.toSet()); assertThat(merged) .contains("Juventus Ronaldo Serie A", "Barcelona Messi La Liga", "Liverpool Salah Premier League", "PSG");
3.5. mergeToList()
mergeToList() takes multiple streams as input. It combines the value of the same index from each stream into a List:
Stream<String> streamOfClubs = Stream .of("Juventus", "Barcelona", "PSG"); Stream<String> streamOfPlayers = Stream .of("Ronaldo", "Messi"); Stream<List<String>> mergedStreamOfList = StreamUtils .mergeToList(streamOfClubs, streamOfPlayers); List<List<String>> mergedListOfList = mergedStreamOfList .collect(Collectors.toList()); assertThat(mergedListOfList.get(0)) .containsExactly("Juventus", "Ronaldo"); assertThat(mergedListOfList.get(1)) .containsExactly("Barcelona", "Messi"); assertThat(mergedListOfList.get(2)) .containsExactly("PSG");
3.6. interleave()
interleave() creates alternates values taken from multiple streams using a selector.
The method gives a set containing one value from each stream to the selector, and the selector will select one value.
Then the selected value will be removed from the set and replaced with the next value from which the selected value originated. This iteration continues until all sources run out of values.
The next example uses interleave() to create alternating values with a round-robin strategy:
Stream<String> streamOfClubs = Stream .of("Juventus", "Barcelona", "Liverpool"); Stream<String> streamOfPlayers = Stream .of("Ronaldo", "Messi"); Stream<String> streamOfLeagues = Stream .of("Serie A", "La Liga"); List<String> interleavedList = StreamUtils .interleave(roundRobinSelector, streamOfClubs, streamOfPlayers, streamOfLeagues) .collect(Collectors.toList()); assertThat(interleavedList) .containsExactly("Juventus", "Ronaldo", "Serie A", "Barcelona", "Messi", "La Liga", "Liverpool");
Be aware that the above code is for tutorial purpose because round-robin selector is provided by the library as Selectors.roundRobin().
3.7. skipUntil() and skipWhile()
skipUntil() skips the values until a value meets the condition:
Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; List skippedUntilGreaterThan5 = StreamUtils .skipUntil(stream(numbers), i -> i > 5) .collect(Collectors.toList()); assertThat(skippedUntilGreaterThan5).containsExactly(6, 7, 8, 9, 10);
In contrast, skipWhile() skips the values while the values meet the condition:
Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; List skippedWhileLessThanEquals5 = StreamUtils .skipWhile(stream(numbers), i -> i <= 5 || ) .collect(Collectors.toList()); assertThat(skippedWhileLessThanEquals5).containsExactly(6, 7, 8, 9, 10);
One important thing about skipWhile() is that it will continue streaming after it found the first value that does not meet the condition:
List skippedWhileGreaterThan5 = StreamUtils .skipWhile(stream(numbers), i -> i > 5) .collect(Collectors.toList()); assertThat(skippedWhileGreaterThan5).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
In Java 9 onward, dropWhile() in standard Stream API provides the same functionality as skipWhile().
3.8. unfold()
unfold() generates a potentially infinite stream by applying a custom generator to a seed value and then to each generated value – the stream can be terminated by returning Optional.empty():
Stream<Integer> unfolded = StreamUtils .unfold(2, i -> (i < 100) ? Optional.of(i * i) : Optional.empty()); assertThat(unfolded.collect(Collectors.toList())) .containsExactly(2, 4, 16, 256);
3.9. windowed()
windowed() creates multiple subsets of source stream as a stream of List. The method takes a source stream, window size and skip value as the parameter.
The List length equals window size, while skip value determines where the subset begin relative to the previous subset:
Integer[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 }; List<List> windowedWithSkip1 = StreamUtils .windowed(stream(numbers), 3, 1) .collect(Collectors.toList()); assertThat(windowedWithSkip1) .containsExactly(asList(1, 2, 3), asList(2, 3, 4), asList(3, 4, 5), asList(4, 5, 6), asList(5, 6, 7));
In addition, the last window is guaranteed to be of the desired size, as we can see in the following example:
List<List> windowedWithSkip2 = StreamUtils.windowed(stream(numbers), 3, 2).collect(Collectors.toList()); assertThat(windowedWithSkip2).containsExactly(asList(1, 2, 3), asList(3, 4, 5), asList(5, 6, 7));
3.10. aggregate()
There are two aggregate() methods that work quite differently.
The first aggregate() groups together elements of equal value according to a given predicate:
Integer[] numbers = { 1, 2, 2, 3, 4, 4, 4, 5 }; List<List> aggregated = StreamUtils .aggregate(Arrays.stream(numbers), (int1, int2) -> int1.compareTo(int2) == 0) .collect(Collectors.toList()); assertThat(aggregated).containsExactly(asList(1), asList(2, 2), asList(3), asList(4, 4, 4), asList(5));
The predicate receives the values in a contiguous manner. Therefore, the above will give a different result if the number is not ordered.
On the other hand, the second aggregate() is simply used to group together elements from the source stream into groups of the desired size:
List<List> aggregatedFixSize = StreamUtils .aggregate(stream(numbers), 5) .collect(Collectors.toList()); assertThat(aggregatedFixSize).containsExactly(asList(1, 2, 2, 3, 4), asList(4, 4, 5));
3.11. aggregateOnListCondition()
aggregateOnListCondition() groups values based on predicate and current active group. The predicate is given the currently active group as a List and the next value. It then must determine if the group should continue or start a new group.
The following example solves a requirement to group contiguous integer values together in a group, where the sum of values in each group must not be greater than 5:
Integer[] numbers = { 1, 1, 2, 3, 4, 4, 5 }; Stream<List<Integer>> aggregated = StreamUtils .aggregateOnListCondition(stream(numbers), (currentList, nextInt) -> currentList.stream().mapToInt(Integer::intValue).sum() + nextInt <= 5); assertThat(aggregated) .containsExactly(asList(1, 1, 2), asList(3), asList(4), asList(4), asList(5));
4. Streamable<T>
An instance of Stream isn’t reusable. For this reason, Streamable provides reusable streams by wrapping and exposing the same methods as the Stream:
Streamable<String> s = Streamable.of("a", "b", "c", "d"); List<String> collected1 = s.collect(Collectors.toList()); List<String> collected2 = s.collect(Collectors.toList()); assertThat(collected1).hasSize(4); assertThat(collected2).hasSize(4);
5. CollectorUtils
CollectorUtils complements the standard Collectors by adding several useful collector methods.
5.1. maxBy() and minBy()
maxBy() finds the maximum value in a stream using supplied projection logic:
Stream<String> clubs = Stream.of("Juventus", "Barcelona", "PSG"); Optional<String> longestName = clubs.collect(CollectorUtils.maxBy(String::length)); assertThat(longestName.get()).isEqualTo("Barcelona");
In contrast, minBy() finds the minimum value using the supplied projection logic.
5.2. unique()
The unique() collector does a very simple thing: it returns the only value if a given stream has exactly 1 element:
Stream<Integer> singleElement = Stream.of(1); Optional<Integer> unique = singleElement.collect(CollectorUtils.unique()); assertThat(unique.get()).isEqualTo(1);
Otherwise, unique() will throw an exception:
Stream multipleElement = Stream.of(1, 2, 3); assertThatExceptionOfType(NonUniqueValueException.class).isThrownBy(() -> { multipleElement.collect(CollectorUtils.unique()); });
6. Conclusion
In this article, we learned how Protonpack library expands the Java Stream API to make it easier to use. It adds useful methods that we might commonly use but are missing from the standard API.
Starting with Java 9, some of the functionality provided by Protonpack will be available in the standard Stream API.
As usual, the code can be found over on Github.