February 6, 2014

Java 8 Streams API - Intermediate Operations

Last couple of posts, we are having an overview of Java 8 Streams API. Till now we have looked at the basics of the streams, understood how the streams work, ways of creating and working with streams and learned about streams laziness and its performance optimization. 


If you are new to Java 8 Streams, go back and read the previous two posts Understanding Java 8 Streams API and Java 8 Streams API - Laziness and Performance Optimization.

During the last discussion, we have understood that any Stream Operation can be divided into the following steps

  1. Creating a Stream. Streams can be created from an existing collection or the other ways of creating Streams.
  2. Set of Intermediate Operations. Intermediate Operations process over a Stream and return Stream as a response.
  3. A Terminal Operation. Terminal Operation is the end of a Stream flow.

For this tutorial we will focus on the various Intermediate Operations made available by the Java 8 Streams API. Examples used in previous posts demonstrates few of the Intermediate Operations like map, filter. Here will have a look at all of them in detail.


Mapping:


Mapping is a process of changing the form of the elements in a stream. In SQL select we choose a particular column, may be in a specific format from a table while leaving the other. Similar capability has been introduced into the Stream operations with the mapping functions.







Map:


Sometimes we need to change the form of an element or completely change the element in a stream. The map is a stream operation which takes a another function as an argument. The function should take each element of a stream as a parameter and return newly created/modified element as a response. The given function is then applied to each element of the stream. Let's have a look at it in the below example.



students.stream()
        .map(Student::getName)
        .forEach(System.out::println);


Here, we have Stream of Student objects. In the map function we are returning the name of the each Student, finally the forEach method will print the names of all of the Students in the stream. The Student::getName is a shorthand notation for a Method Reference, introduced in Java 8. For more about Method References please read: 'At First Sight' With Closures in Java.

When the stream was created out of the Student collection, it was of a type Stream of Students. In the map function the return type of the getName method is String, and hence the Stream coming out of the map function will be of a type Stream of Strings.


FlatMap:


The flatMap transforms each element of a stream into another form (just like map), and generates sub streams of the newly formed elements. Finally, it flattens all of the sub streams into a single stream of elements. As the flatMap is a map type of function, it also takes a function and applies (maps) that function to each of the element in the stream

The difference between map and flatMap is, the map accepts a function that returns a mapped element and then the map function returns a stream of such elements. On the other hand, the flatMap accepts a function that returns streams of the mapped elements and then the flatMap finally returns a collective stream of all of the sub streams that are created by the each execution of the passed function. Let's have a look at the below example, which clearly shows how the flatMap works and also also shows the difference between the map and flatMap when passed a similar function.



List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<List<Integer>> mapped = 
                     numbers.stream()
                     .map(number -> Arrays.asList(number -1, number, number +1))
                     .collect(Collectors.toList());
System.out.println(mapped); //:> [[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5]]
        
List<Integer> flattened = 
                numbers.stream()
                .flatMap(number -> Arrays.asList(number -1, number, number +1).stream())
                .collect(Collectors.toList());
System.out.println(flattened);  //:> [0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5]




Let's have a look at what happened with the map function here. The function passed to the map takes an integer and returns a List of three integers (number-1, number, and number +1). The map function returns stream of List of Integers. When the stream is collected in a List the output we get is a List of List of Integers. We did almost the similar operation with the flatMap, with the only difference that the function passed to the flatMap returns a stream. All of these streams are collected into a single stream and once collected into a List we get a List of Integers. The concept of flattening is well known in the functional world and very useful when you want to flatten the collections of collections.




Filtering:



The Java 8 Streams API provides lots of methods, Which helps to deal with Collections the way SQL operations deal with a SELECT query. We will have a look at these methods in details.


Filter:


The filter method is used to filter out elements from a stream, depending upon some condition. The filter method accepts a Predicate as an argument. A Predicate is a function that returns boolean. The filter method returns a stream containing the elements matching to the given predicate.



//Only the students with score >= 60
students.stream()
            .filter(student -> student.getScore() >= 60)
            .collect(Collectors.toList());



Unique Elements:


The function distinct returns a stream containing unique elements only. This is a very easy way to remove duplicates from a collection. The distinct method uses equals method for checking the equality and the custom objects would require an implementation of the equals method.



//Get distinct list of names of the students
students.stream()
            .map(Student::getName)
            .distinct()
            .collect(Collectors.toList());

The distinct operation in Java 8 Streams API is a buffered operation. To perform this operation over a stream it needs all of the elements of the stream at one place, before any element is actually written to the output stream. This would eat lot of memory space if a stream is too large.

Limiting:

The limit method is used to limit the number of elements in a stream. Number of required elements is passed to the limit function as an argument. The limit is a short circuiting operation, the stream is just skipped, once the limit condition is satisfied. Please refer to the last post, to know more about Java 8 Steams short circuiting operations.


//List of first 3 students who have age > 20
students.stream()
            .filter(s -> s.getAge() > 20)
            .map(Student::getName)
            .limit(3)
            .collect(Collectors.toList());




Skipping:

The skip method is used to skip the given number of elements from the stream. The skipped elements will not be a part of the the returning stream. If number of elements in the stream are less than or equal to the number of elements to be skipped, an empty stream is returned.


//List of all the students who have age > 20 except the first 3
students.stream()
            .filter(s -> s.getAge() > 20)
            .map(Student::getName)
            .skip(3)
            .collect(Collectors.toList());




Sorting:


This is another very important operation on the Java 8 Steams API. Frequently we see a requirement to get a sorted collection. Java streams API also has very easy to use sorting method. 

Below example shows the stream of students is mapped to the student names and then there is a sort method, which returns the sorted stream of student names. Remember, the sort method does't take any parameter here, and hence it will sort the list in natural order.




students.stream()
           .map(Student::getName)
           .sorted()
           .collect(Collectors.toList());



Here is how can we provide our own sorting logic. The comparing and few other useful methods have been added to the Comparator. Here is an example how the comparing method is used to provide a custom sorting logic. The output of the below code is exactly the same as above.




students.stream()
           .sorted(Comparator.comparing(Student::getName))
           .map(Student::getName)
           .collect(Collectors.toList());




Sorting is not limited to the comparing method. We can write even more complex logic for sorting. Below are few of the code examples, showing how easily this can be done




//Sorting names if the Students in descending order
students.stream()
           .map(Student::getName)
           .sorted(Comparator.reverseOrder())
           .collect(Collectors.toList());


//Sorting names if the Students in descending order
students.stream()
           .map(Student::getName)
           .sorted(Comparator.reverseOrder())
           .collect(Collectors.toList());


//Sorting students by First Name and Last Name both
students.stream()
           .sorted(Comparator.comparing(Student::getFirstName).
                              thenComparing(Student::getLastName))
           .map(Student::getName)
           .collect(Collectors.toList());



//Sorting students by First Name Descending and Last Name Ascending
students.stream()
           .sorted(Comparator.comparing(Student::getFirstName)
                              .reversed()
                              .thenComparing(Student::getLastName))
           .map(Student::getName)
           .collect(Collectors.toList());


Just like distinct, the sort function is also buffered, and requires all of the elements of a stream before it actually performs the sorting.



Friends, here we are done with an overview of Java 8 Streams API - Intermediate Operations. These are really quick and easy methods that have very powerful capabilities. Java 8 Streams API is not over for us yet. We will still continue knowing more about the API. See you soon in the next post. 




Other Java 8 and Streams API Articles: