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:
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:
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 |
---|---|
|
Returns a |
|
Returns a |
|
Returns a |
|
Returns a |
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.