Become a Master of Java Streams – Part 2: Intermediate Operations
Just like a magic wand, an Intermediate operation transforms a Stream into another Stream. These operations can be combined in endless ways to perform anything from simple to highly complex tasks in a readable and efficient manner.
This article is the second out of five, complemented by a GitHub repository containing instructions and exercises to each unit.
- Part 1: Creating Streams
- Part 2: Intermediate Operations
- Part 3: Terminal Operations
- Part 4: Database Streams
- Part 5: Creating a Database Application Using Streams
Intermediate Operations
Intermediate operations act as a declarative (functional) description of how elements of the Stream should be transformed.Together, they form a pipeline through which the elements will flow. What comes out at the end of the line, naturally depends on how the pipeline is designed.
As opposed to a mechanical pipeline, an intermediate operation in a Stream pipeline may(*) render a new Stream that may depend on elements from previous stages. In the case of a map-operation (which we will introduce shortly) the new Stream might even contain elements of a different type.
(*) Strictly speaking, an intermediate operation is not mandated to create a new Stream. Instead, it can update its internal state or, if the intermediate operation did not change anything (such as.skip(0)
) return the existing Stream from the previous stage.
To get a glimpse of what a pipeline can look like, recall the example used in the previous article :
1 2 3 4 5 6 7 | List<String> list = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .filter(s -> s.startsWith( "L" )) .map(String::toUpperCase) .sorted() .collect(toList()); System.out.println(list); |
1 | [LEMUR, LION] |
We will now go on to explain the meaning of these and other operations in more detail.
Filter
Based on our experience, filter()
is one of the most useful operations of the Stream API. It enables you to narrow down a Stream to elements that fit certain criteria. Such criteria must be expressed as a Predicate
(a function resulting in a boolean
value) e.g. a lambda. The intention of the code below is to find the Strings that start with the letter “L” and discard the others.
1 2 3 4 5 | Stream<String> startsWithT = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .filter(s -> s.startsWith( "L" )); |
1 | startsWithT: [Lion, Lemur] |
Limit
There are some very simple, but yet powerful, operations that provide a way to select or discard elements based on their position in the Stream. The first of these operations is limit(n)
which basically does what it says – it creates a new stream that only contains the first n elements of the stream it is applied on. The example below illustrates how a Stream of four animals is shortened to only “Monkey” and “Lion”.
1 2 3 4 | Stream<String> firstTwo = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .limit( 2 ); |
1 | firstTwo: [Monkey, Lion] |
Skip
Similarly, if we are only interested in some of the elements down the line, we can use the.skip(n)
-operation. If we applyskip(2)
to our Stream of animals, we are left with the tailing two elements “Giraffe” and “Lemur”.
1 2 3 4 | Stream<String> firstTwo = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .skip( 2 ); |
1 | lastTwo: [Giraffe, Lemur] |
Distinct
There are also situations where we only need one occurrence of each element of the Stream. Rather than having to filter out any duplicates manually, a designated operation exists for this purpose –distinct()
. It will check for equality using Object::equals
and returns a new Stream with only unique elements. This is akin to a Set.
1 2 3 4 | Stream<String> uniqueAnimals = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" , "Lion" ) .distinct(); |
1 | uniqueAnimals: [“Monkey”, “Lion”, “Giraffe”, “Lemur”] |
Sorted
Sometimes the order of the elements is important, in which case we want control over how things are ordered. The simplest way to do this is with the sorted-operation which will arrange the elements in the natural order. In the case of the Strings below, that means alphabetical order.
1 2 3 4 | Stream<String> alphabeticOrder = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .sorted(); |
1 | alphabeticOrder: [Giraffe, Lemur, Lion, Monkey] |
Sorted with comparator
Just having the option to sort in natural order can be a bit limiting sometimes. Luckily, it is possible to apply a custom Comparator
to inspect a certain property of the element. We could for example order the Strings after their lengths accordingly:
1 2 3 4 | Stream<String> lengthOrder = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .sorted(Comparator.comparing(String::length)); |
1 | lengthOrder: [Lion, Lemur, Monkey, Giraffe] |
Map
One of the most versatile operations we can apply to a Stream is map()
. It allows elements of a Stream to be transformed into something else by mapping them to another value or type. This means the result of this operation can be a Stream of any type R
. The example below performs a simple mapping from String
to String
, replacing any capital letters with their lower case equivalent.
1 2 3 4 | Stream<String> lowerCase = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .map(String::toLowerCase); |
1 | lowerCase: [monkey, lion, giraffe, lemur] |
Map to Integer, Double or Long
There are also three special implementations of the map-operation which are limited to mapping elements to the primitive types int
, double
andlong
.
1 2 3 | .mapToInt(); .mapToDouble(); .mapToLong(); |
Hence, the result of these operations always corresponds to an IntStream
, DoubleStream
or LongStream
. Below, we demonstrate how .mapToInt()
can be used to map our animals to the length of their names:
1 2 3 4 | IntStream lengths = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .mapToInt(String::length); |
1 | lengths: [ 6 , 4 , 7 , 5 ] |
Note:String::length
is the equivalent of the lambda s -> s.length()
. We prefer the former notation since it makes the code more concise and readable.
FlatMap
The last operation that we will cover in this article might be more tricky to understand even though it can be quite powerful. It is related to the map()
operation but instead of taking a Function
that goes from a type T
to a return type R
, it takes a Function
that goes from a type T
and returns a Stream
of R
. These “internal” streams are then flattened out to the resulting streams resulting in a concatenation of all the elements of the internal streams.
1 2 3 4 | Stream<Character> chars = Stream.of( "Monkey" , "Lion" , "Giraffe" , "Lemur" ) .flatMap(s -> s.chars().mapToObj(i -> ( char ) i)); |
1 | chars: [M, o, n, k, e, y, L, i, o, n, G, i, r, a, f, f, e, L, e, m, u, r] |
Exercises
If you haven’t already cloned the associated GitHub repo we encourage you to do so now. The content of this article is sufficient to solve the second unit which is called MyUnit2Intermediate
. The corresponding Unit2Intermediate
Interface contains JavaDocs which describes the intended implementation of the methods in MyUnit2MyIntermediate
.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | public interface Unit2Intermediate { /** * Return a Stream that contains words that are * longer than three characters. Shorter words * (i.e. words of length 0, 1, 2 and 3) * shall be filtered away from the stream. * <p> * A Stream of * ["The", "quick", "quick", "brown", "fox", * "jumps", "over", "the", "lazy", "dog"] * would produce a Stream of the elements * ["quick", "quick", "brown", "jumps", * "over", "lazy"] */ Stream<String> wordsLongerThanThreeChars(Stream<String> stream); |
The provided tests (e.g. Unit2MyIntermediateTest
) will act as an automatic grading tool, letting you know if your solution was correct or not.
Next Article
In the next article, we proceed to terminal operations and explore how we can collect, count or group the resulting elements of our pipeline. Until then – happy coding!
Authors
Per Minborg and Julia Gustafsson
Published on Java Code Geeks with permission by Per Minborg, partner at our JCG program. See the original article here: Become a Master of Java Streams – Part 2: Intermediate Operations Opinions expressed by Java Code Geeks contributors are their own. |