In the last post in this series, we took a look at the new convenience factory methods for collections in Java 9. Collections play well with the Stream API, which learned a few new tricks since its introduction in Java 8 as well. There are 4 particularly exciting new features – let’s explore them with JShell!
1. dropWhile
and takeWhile
Let’s take a step back and look at what was available prior to Java 9. We had an API for making streams containing elements. For example, let’s create a stream of numbers 0 to 9:
Since Java 8, we’ve known limit(long n)
and its complementary method skip(long n)
, which allow us to take and ignore the first n
elements of a stream, respectively. For instance:
These methods are very useful. However, they require us to know the number of elements to limit or skip in advance. Often, we know we want to keep taking or dropping elements while a certain condition is met, but we don’t necessarily know how many of these element we get. That’s exactly the functionality added in Java 9, courtesy of takeWhile
and dropWhile
methods:
takeWhile
and dropWhile
are complementary to each other. They’re basically versions of limit
and skip
which take a predicate instead of a number.
2. iterate
Another useful addition to the Stream API is the method iterate
. The method is overloaded:
The version on line 3 was available in JDK 8, whereas the version on line 4 was added in JDK 9. To illustrate the difference, let’s try and solve a simple problem – suppose we want to print all even numbers less than 10.
A naive approach to solving this problem using Java 8 constructs could involve iteration with a filtering step, such as:
As we can see, if we try to run this simple and seemingly straightforward code, we discover it doesn’t work. At first, the correct solution is printed, then the output pauses, and continues to print incorrect numbers after a while.
It’s easy to see what’s happening here. We construct the right filter, but apply it to an infinite stream. After printing the elements matching the filtering condition, the stream silently keeps going. Our integers overflow and start matching the filtering condition again.
Unfortunately, there isn’t a concise and clean way to deal with this situation in JDK 8, which is what the recent release of the JDK fixes. We can now correctly solve our problem as follows:
If we look at the solution, we can see this version of iterate
reads like a for
loop, and that’s essentially what it is – a streamified version of for
.
3. ofNullable
Another addition to the Stream API worth talking about is the method ofNullable
. Prior to Java 9, we could create streams of arbitrary elements using the factory methods. For example:
The result is a stream containing a single element – 1
. We couldn’t do the same thing with null
though:
This makes sense – we don’t want to have null
s in our streams. Java 9, however, allows us to do the following:
The result is not a stream containing a null
element, but an empty stream:
If you’ve worked with streams a lot, you probably immediately realize why this is an exciting feature. It makes integration better, and allows us to construct cleaner streams. Often, when using long streams with many chained operations, particularly map
s, we end up with null
s at some point in our stream. Since we don’t want to have null
s in streams, or NullPointerException
s, we need to filter the null
elements out. In practice, this involves a step with a ternary operator or an if
statement, none of which are nice. ofNullable
enables us to avoid these null
checks, which is great!
4. Collectors.filtering
and Collectors.flatMapping
Collectors are an essential part of the Stream API. They provide reduction operations which process stream elements into result containers, optionally performing a transformation on the aggregated results, after all input elements have been processed.
Java 9 added 2 new collectors, filtering
and flatMapping
, which play nice with the groupingBy
collector.
Collectors.filtering
Similarly to the filter
method on streams, the filtering
collector is used for filtering elements in a stream. The filter
method, however, processes the values before they’re grouped, whereas Collectors.filtering
can be used nicely with Collectors.groupingBy
to group the values before the filtering step takes place.
Let’s demonstrate the difference on an example. Suppose we’re given a list of numbers, e.g.:
Now suppose we need to count the numbers by parity, but we’re only interested in the numbers greater than 5. Prior to Java 9, we would accomplish this as follows:
The result correctly says that we have 3 odd numbers greater than 5 in our list. However, it doesn’t tell us we don’t have any even numbers there. The even numbers were filtered out before aggregation, and any trace of them is lost. That’s exactly what the filtering
collector addresses. In Java 9, we can count the numbers in both bags as follows:
As we can see now, there are clearly no even and 3 odd numbers matching our condition.
Collectors.flatMapping
Collectors.flatMapping
is to Collectors.mapping
what Stream.flatmap
is to Stream.map
. Like the flatmap
operation, the flatMapping
collector allows us to handle nested collections (collections in streams) better, in this case on the collectors’ side. Like Collectors.mapping
, Collectors.flatMapping
takes a function to be applied to the input elements, and a collector to accumulate the elements passed through the function. Unlike Collectors.mapping
, however, Collectors.flatMapping
deals with a stream of elements, which allows us to get rid of often unnecessary intermediary collections.
Consider a stream of some data entries, for example integers associated with collections of strings, such as the following:
Now suppose we want to aggregate this data, e.g. group the strings by the integers they’re associated with. Using Java 8 constructs, we would accomplish this as follows:
Although this is technically correct, we end up with nested collections, which we would need to further unwrap to, for example, deal with duplicates. Fortunately, that’s exactly what the new flatMapping
collector is for:
To sum up, the updates to the Stream API introduced in Java 9 are not huge. They do, however, tie up some loose ends, and ultimately allow us to write cleaner code, which is always exciting!
Comments