Chapter 9 - Java for Beginners Course

Intermediate Operations

The intermediate operations in a stream pipeline are the ones that define how we want to process the data as it arrives.

In this section we’ll cover some of the main intermediate operations that are provided by the Stream interface. We’ll not cover all of the operations defined in the API of the Stream interface but will provide an introduction to the most common ones.

Filter

One of these common operations is to remove elements from a Stream that we’re not interested in. As in our example from the previous section where we only want to keep the even numbers from our original stream:

Example of an Integer Stream of 4 elements with a filter

To achieve this, we can make use of the filter method as follows:

Integer[] array = { 1, 2, 4, 5 };
Stream<Integer> numbers = Stream.of(array) // create the stream
        .filter(n -> n % 2 == 0); // apply a filter

Map

A mapping operation takes the elements of the stream as an input, applies an operation (function) to each element and returns a new stream with the result of the operation.

For example, if we want to change the sign of the numbers in the example above we can apply the following map operation:

Integer[] array = { 1, 2, 4, 5 };
Stream<Integer> numbers = Stream.of(array)
        .map(number -> number * -1);

This would result in the following:

Example of an Integer Stream of 4 elements with a map operation
Even though they have the same name, the map operation is not related to the Map<K,V> data structure that we covered in Chapter 7.

Sorted, Distinct, Skip and Limit

These 4 operations do what you’ll expect and will return a new Stream with the following properties:

Operation Description

sorted()

Returns a Stream with the elements of the original Stream sorted by their natural order. There is another version of this method that accepts a Comparator if you want to sort elements in a different way.

distinct()

Returns a Stream that filters out any repeated elements from the original Stream. The distinct operation relies on the equals method of the objects in the Stream.

skip(n)

Returns a Stream that doesn’t contain the first n elements from the original Stream.

limit(n)

Returns a Stream that contains at most n elements from the original Stream.

Multiple intermediate operations

As mentioned in the previous section, a stream pipeline can define zero of more intermediate operations. To define multiple intermediate operations we can chain method calls as we’ll see below.

Let’s assume we want to take the stream from our first example and 1) apply the change of sign, 2) sort the elements and 3) get the lowest 2 elements:

Integer[] array = { 1, 2, 4, 5 };
Stream<Integer> numbers = Stream.of(array)
        .map(number -> number * -1)
        .sorted()
        .limit(2)

Good practices when working with Streams

As you can see from the examples above we tend to put each operation in a separate line. This is a common practice that is used to help with the readability of code that defines stream pipelines.

A second common practice is to avoid very long bodies inside an operation in a stream pipeline. Instead, you can define a method and use a method reference. This also to help with readability. For example, let’s assume we have the following code:

Integer[] array = { 1, 2, 4, 5 };
Stream<Integer> nums = Stream.of(array)
        .map(n -> {
            if (n % 2 == 1) {
                n = n + 1;
            }
            return n;
        });

The map operation in the middle is changing any odd number (e.g. 1) and adding 1 to them. As such, we could rewrite this as:

public static void main(String args[]) {
    //...
    Stream<Integer> nums = Stream.of(array)
        .map(MyClass::addOneIfOdd);
    //...
}

private static Integer addOneIfOdd(Integer n) {
    if (n % 2 == 1) {
        n = n + 1;
    }
    return n;
}

Important note about Intermediate Operations

As we covered in the previous section, Stream instances are lazy and won’t perform any operations until we provide a terminal operation. For example, consider the following code:

Integer[] array = { 1, 2, 4, 5 };
Stream<Integer> numbers = Stream.of(array)
        .filter(n -> {
            System.out.println("Processing element: " + n);
            return (n % 2 == 0);
        });

A common misconception is that this will print all the elements in the stream. However, with the code as it is, the stream pipeline won’t be executed and no processing will be done.

Once we add a terminal operation, the processing will start and the output will occur as expected. We’ll cover terminal operations in the next section.