Concatenate Streams and Reduction with Streams


Concatenate Streams

Variable declaration for examples:

Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");

Example 1 - Concatenate two Streams

final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());
 
concat1.forEach(System.out::print);
// prints: abc123

Example 2 - Concatenate more than two Streams

final Stream<String> concat2 = Stream.concat(
           Stream.concat(abc.stream(), digits.stream()),
           greekAbc.stream());
 
System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Alternatively to simplify the nested concat() syntax the Streams can also be concatenated with flatMap():

final Stream<String> concat3 = Stream.of(abc.stream(), digits.stream(), greekAbc.stream())
	.flatMap(s -> s);
	// or `.flatMap(Function.identity());` (java.util.function.Function)
 
System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Be careful when constructing Streams from repeated concatenation, because accessing an element of a deeply concatenated Stream can result in deep call chains or even a StackOverflowException.


Reduction with Streams

Reduction is the process of applying a binary operator to every element of a stream to result in one value.

The sum() method of an IntStream is an example of a reduction; it applies addition to every term of the Stream, resulting in one final value:

This is equivalent to (((1+2)+3)+4)

The reduce method of a Stream allows one to create a custom reduction. It is possible to use the reduce method to implement the sum() method

IntStream istr;
 
//Initialize istr
 
OptionalInt istr.reduce((a,b)->a+b)

The Optional version is returned so that empty Streams can be handled appropriately.

Another example of reduction is combining a Stream<LinkedList<T>> into a single LinkedList<T>:

Stream<LinkedList<T>> listStream;
 
//Create a Stream<LinkedList<T>>
 
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
	LinkedList<T> retList = new LinkedList<T>();
	retList.addAll(list1);
	retList.addAll(list2);
	return retList;
});

You can also provide an identity element. For example, the identity element for addition is 0, as x+0==x. For multiplication, the identity element is 1, as x*1==x. In the case above, the identity element is an empty LinkedList<T>, because if you add an empty list to another list, the list that you are "adding" to doesn't change:

Stream<LinkedList<T>> listStream;
 
//Create a Stream<LinkedList<T>>
 
LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
	LinkedList<T> retList = new LinkedList<T>();
	retList.addAll(list1);
	retList.addAll(list2);
	return retList;
});

Note that when an identity element is provided, the return value is not wrapped in an Optional—if called on an empty stream, reduce() will return the identity element.

The binary operator must also be associative, meaning that (a+b)+c==a+(b+c). This is because the elements may be reduced in any order. For example, the above addition reduction could be performed like this:

This reduction is equivalent to writing ((1+2)+(3+4)). The property of associativity also allows Java to reduce the Stream in parallel—a portion of the Stream can be reduced by each processor, with a reduction combining the result of each processor at the end.


Using Streams of Map.Entry to Preserve Initial Values after Mapping

When you have a Stream you need to map but want to preserve the initial values as well, you can map the Stream to a Map.Entry<K,V> using a utility method like the following:

public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
    return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}

Then you can use your converter to process Streams having access to both the original and mapped values:

Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
       .map(entryMapper(transformer));

You can then continue to process that Stream as normal. This avoids the overhead of creating an intermediate collection.

Basic Programs