Friday, January 1, 2016

Java 8 - Streams (Part - II)

In the previous post, We learnt - What are Streams, How to create, How to Use and What are the operations that can be performed against them. Now we will continue on operations to get insights about the Streams.

Terminal and Intermediate Operations

Operations on Streams are two types. One is Intermediate Operation which returns a stream on which we can perform another operation. Second is Terminal Operation which returns a result other than stream. In the last post, we saw forEach operation which is a terminal operation, whereas sort operation which is a intermediate operation because it returns an another stream. Now, we will see some other important operations (both terminal and intermediate operations).

Count

Count operation is a Terminal operation which returns the number of the elements in the current Stream.
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2");
System.out.println(list.stream().collect()); // This prints the value of 6

Reduce

Reduce operation (Terminal operation) reduces the elements in a stream using an operation. See this
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2");
 list.stream().reduce((s1,s2) -> s1 + "-"+ s2 ).ifPresent(System.out::println); //prints a1-a2-b-b-c1-c2

Filter

Filter operation (Intermediate Operation) filters some of the elements in the stream based on the operation passed to it.
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2");
list.stream().filter(a -> a.startsWith("a")).forEach(System.out::println); //prints a1 and a2

Match

Match Operation (Terminal Operation) returns boolean value based on the operation associated with it. We can do three match operations : anyMatch, allMatch and noneMatch. Each of these returns a value (true or false) based on the match criteria.
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2");
System.out.println(list.stream().allMatch((a) -> a.startsWith("a")));  // false
System.out.println(list.stream().anyMatch((a) -> a.startsWith("a")));  // true
System.out.println(list.stream().noneMatch((a) -> a.startsWith("d"))); // true

Map

Map Operation (Intermediate Operation) converts each of the element in stream into another object via the operation passed to it. When this map operation combined with other operations like filter, reduce, forEach etc.. gives trivial results.
See below an example of how to make all the elements in the stream which start with "b" to upper case.
List<String> list = Arrays.asList("apple","biscuit","blah","cupcake","cat");
list.stream().filter(a -> a.startsWith("b")).map(String::toUpperCase).forEach(System.out::println);

Collect

Collect is one of the most important feature/method of the Stream class. Collect is used to convert the Stream into a List, Set or Map. Collect method accepts a Collector which can perform operations like Supplier, Combiner, Accumulator or Finisher. Let's look at some of them.
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2");
Set<String> set = list.stream().collect(Collectors.toSet());
set.stream().forEach(System.out::println);
Above, we are trying to convert a given stream into a Set (As we know the property of Set - it doesn't hold duplicate values. So b will be dropped when it prints the values on the console.
Look at, another example how to group the stream using collect method
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2");
Map<Integer,List<String>> map = list.stream().collect(Collectors.groupingBy((String p) -> p.length()));
System.out.println(map);
The above code prints the data to console as
{1=[b, b], 2=[a1, a2, c1, c2]}
This also helps to summarize the operations like min, max, average, count etc. Let's see how to do that
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2");
IntSummaryStatistics stats = list.stream().collect(Collectors.summarizingInt((String t) -> t.length()));
System.out.println(stats); // This will print IntSummaryStatistics{count=6, sum=10, min=1, average=1.666667, max=2}
So far we have seen some built-in Collectors, now we will see how to create a collector with all of it's features (Supplier, Combiner, Accumulator and Finisher)
List<String> list = Arrays.asList("a1","a2","b","b","c1","c2"); 
Collector<String, StringJoiner, String> sCollector = 
      Collector.of(() -> new StringJoiner(" | "),        // supplier
      (j, p) -> j.add(p),                               // accumulator
      (j1, j2) -> j1.merge(j2),                         // combiner
      StringJoiner::toString);                          // finisher
        
String str = list.stream().collect(sCollector);
System.out.println(str); // Prints a1 | a2 | b | b | c1 | c2
The output simply explains what is being done using the streams (merging the strings using StringJoiner with pipe as a delimiter). This works more better if we are using it on objects with some calculations on it instead of strings.

Parallel Streams

Parallel Operations in Java made easier after the introduction of Fork/Join framework. To do that, we need to implement the same in our programming. In Streams it's very easy, just use .parallelStream to get the parallel stream instead of .stream in each of the examples above and make parallel operations. So, depending on the type of operation and requirement either we can call a sequential stream (using stream() method) or parallel stream (using parallelStream() method). Try replacing all the above examples with parallelStream and see the results.

Happy Learning!!!