Java SE 8/11 Programmer II Exam Series: Advanced Stream Pipeline Concepts

Introduction

In this section, we will cover advanced stream pipeline concepts in Java 8. We will analyze chaining Optionals, predefined collectors, and learn how to use groupingBy, partitioningBy, and mapping methods.

Chaining Optionals

Chaining optionals help eliminate the nested if-statements for Optional variables. The function below prints the virus name if it contains Covid-19 without chaining optional. Hopefully, it won’t print 🙂

private static void printVirus(Optional<String> optional) {
	if (optional.isPresent()) {
		String virus = optional.get();
		if (virus.contains("Covid-19"))
			System.out.println(virus);
	}
}

Yet, this is a code smell. If you add more nested if-statements, you will end up with the Arrow Anti Pattern. The following version is more concise and descriptive. If you need to have conditions, you can add a new filter.

private static void printVirusUsingChaining(Optional<String> optional) {
     optional.filter(s -> s.contains("Covid-19"))      
             .ifPresent(System.out::println);
}

Grouping results

In this section, we will have a look at the predefined collectors, and learn how to use groupingBy, partitioningBy, and mapping methods.

Basic operations

Many of the collectors behave in the same way. All we need to do is to pass the collect(Collector<? super T,A,R> collector) method accumulating the elements of a stream into a final result. Let’s look at some of the collectors.

Collectors.joining

Concatenate the string elements, separated by the comma.

Stream<String> streamJoin = Stream.of("the", "new", "normal");
String resultJoin = streamJoin.collect(Collectors.joining(", "));
System.out.println(resultJoin); // the, new, normal

Collectors.averagingInt

Produce the arithmetic mean of the string elements

Stream<String> streamAverage = Stream.of("the", "new", "normal");
Double resultAverage = streamAverage.collect(Collectors.averagingInt(String::length));
System.out.println(resultAverage); // 4.0

Collectors.toCollection

Accumulate a stream into other collections. Sometimes you want to have more control on the return type.

Stream<String> streamCollection = Stream.of("the", "new", "normal");
Set<String> result = streamCollection
                         .filter(s -> s.startsWith("n"))
                         .collect(Collectors.toCollection(HashSet::new));
System.out.println(result); // [new, normal]

Collecting into maps

There are three overloaded functions for Collectors.toMap(). Let’s look at them in detail using an example. We have a CoronavirusCase class below.

public class CoronavirusCase {
	private String country;
	private long numberOfCases;
	public CoronavirusCase(String country, long numberOfCases) {
		super();
		this.country = country;
		this.numberOfCases = numberOfCases;
	}
    //getters and setters
}

Now, we create a list of coronavirus cases and want to convert them to a map using a Collectors class.

List<CoronavirusCase> cases = new ArrayList<>();
cases.add(new CoronavirusCase("TURKEY", 170000));
cases.add(new CoronavirusCase("SPAIN", 242000));
cases.add(new CoronavirusCase("SWEEDEN", 287000));
cases.add(new CoronavirusCase("ITALY", 235000));
cases.add(new CoronavirusCase("USA", 287000));
cases.add(new CoronavirusCase("UK", 287000));

// Create a map using toMap(keyMapper, valueMapper)
// Create a map using toMap(keyMapper, valueMapper, mergeFunction)
// Create a tree map using toMap(keyMapper, valueMapper, mergeFunction, mapSupplier)

Collectors.toMap(keyMapper, valueMapper)

toMap(keyMapper, valueMapper) takes mapping functions to produce keys and values, then returns a Collector which collects elements into a Map. In this example, we choose the country as key and the number of cases as value.

Map<String, Long> map = cases.stream().collect(Collectors.toMap(CoronavirusCase::getCountry, CoronavirusCase::getNumberOfCases));
System.out.println(map);//{USA=287000, TURKEY=170000, UK=287000, ITALY=235000, SWEEDEN=287000, SPAIN=242000}

What if we choose the number of cases as a key?

Map<Long, String> mapDuplicated = cases.stream()
            .collect(Collectors.toMap(CoronavirusCase::getNumberOfCases, CoronavirusCase::getCountry));
System.out.println(mapDuplicated); ////throws Exception in thread "main" java.lang.IllegalStateException: Duplicate key SWEEDEN

We would get the java.lang.IllegalStateException as the number of cases is equal for SWEEDEN, USA, and ITALY.

Collectors.toMap(keyMapper, valueMapper, mergeFunction)

toMap(keyMapper, valueMapper, mergeFunction) function deals with these collisions problems. It takes an additional parameter called a merge function. If the keys have duplicate values, the merge function is applied. The example below produces a map by mapping cases to a concatenated list of countries:

BinaryOperator<String> mergeFunction = (case1, case2) -> case1 + "-" + case2;
Map<Long, String> map2 = cases.stream().collect(Collectors.toMap(CoronavirusCase::getNumberOfCases,
CoronavirusCase::getCountry, mergeFunction));
System.out.println(map2);
// {170000=TURKEY, 242000=SPAIN, 235000=ITALY, 287000=SWEEDEN-USA-UK}

Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapSupplier)

If we wanted to sort the map, we would use the toMap(keyMapper, valueMapper, mergeFunction, mapSupplier) function. It takes mapping functions to produce keys and values, a merge function, and a supplier. The sorted map is created by a provided supplier TreeMap.

BinaryOperator<String> mergeFunction = (case1, case2) -> case1 + "-" + case2;
TreeMap<Long, String> treeMap = 
    cases.stream()
         .collect(Collectors.toMap
                     (CoronavirusCase::getNumberOfCases,
                      CoronavirusCase::getCountry, 
                      mergeFunction, 
                      TreeMap::new)
                 );
System.out.println(treeMap); 
//{170000=TURKEY, 235000=ITALY, 242000=SPAIN, 287000=SWEEDEN-USA-UK}
System.out.println(treeMap.getClass()); 
//class java.util.TreeMap

Grouping, Partitioning, and Mapping

GroupingBy

groupingBy(Function classifier) groups the elements according to a classification function. We grouped the coronavirus cases with respect to the number of cases.

Map<Long, List<CoronavirusCase>> groupingBy = 
cases
 .stream()
 .collect(Collectors.groupingBy(CoronavirusCase::getNumberOfCases));

groupingBy(Function classifier, Collector downstream) groups the elements using a classifier and performs a reduction operation with a downstream collector. We changed the value of the map from List to Set using the downstream collector, Collectors.toSet().

Map<Long, Set<CoronavirusCase>> groupingBy = 
cases
 .stream()
 .collect(Collectors.groupingBy(CoronavirusCase::getNumberOfCases, Collectors.toSet()));

groupingBy(Function classifier, Supplier mapFactory, Collector downstream) groups the elements using a classifier and performs a reduction operation with a collector, and applies the supplier to change the return type. We returned TreeMap instead of Map using TreeMap::new supplier.

TreeMap<Long, List<CoronavirusCase>> groupingBy = 
cases
  .stream()
  .collect(Collectors.groupingBy(CoronavirusCase::getNumberOfCases, TreeMap::new, Collectors.toList()));

PartitioningBy

With partitioning, we split the elements into two groups – true and false.

partitioningBy(Predicate predicate) splits the input elements using a predicate. We partitioned the coronavirus cases into two groups – cases that are less than or equal to or greater than 200000.

Map<Boolean, List<CoronavirusCase>> partitioningBy1 = 
cases
    .stream()
    .collect(Collectors.partitioningBy(s -> s.getNumberOfCases() <= 200000));

partitioningBy(Predicate predicate, Collector downstream) splits the input elements using a predicate and performs reduction. We partitioned the input stream with respect to the number of cases. We also changed the value of the map from List to Set using the downstream collector, Collectors.toSet().

Map<Boolean, Set<CoronavirusCase>> partitioningBy2 = 
cases
  .stream()
  .collect(Collectors.partitioningBy(s -> s.getNumberOfCases() <= 12000, Collectors.toSet()));

Mapping

According to Java Doc, the mapping() collectors are most useful when used in a multi-level reduction, such as a groupingBy or partitioning. We can generalize the mapping formula as follows:

Given a stream of S, accumulate X of Y for/in each Z:

For example, given a stream of CoronavirusCase, accumulate the set of countries names for each cases:

Map<Long, Set<String>> countryByCases = cases.stream()
	.collect(Collectors.groupingBy(CoronavirusCase::getNumberOfCases,
		 Collectors.mapping(CoronavirusCase::getCountry, Collectors.toSet())));
System.out.println(countryByCases);// {170000=[TURKEY], 242000=[SPAIN], 235000=[ITALY], 287000=[USA, UK, SWEEDEN]}

Similarly, given a stream of CoronavirusCase, accumulate the set of cases for each country:

Map<String, Set<Long>> casesByCountry = cases.stream()
	.collect(Collectors.groupingBy(CoronavirusCase::getCountry,
	Collectors.mapping(CoronavirusCase::getNumberOfCases, Collectors.toSet())));
System.out.println(casesByCountry); //{USA=[287000], TURKEY=[170000], UK=[287000], ITALY=[235000], SWEEDEN=[287000], SPAIN=[242000]}

Summary

In this section, we covered the advanced stream pipeline concepts in Java 8. We learned how to chain Optionals, use basic collectors, and studied groupingBy, partitioningBy, and mapping methods. You can find the source code on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *