Minborg

Minborg
Minborg

Tuesday, October 15, 2019

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.

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 :
List<String> list = Stream.of("Monkey", "Lion", "Giraffe","Lemur")
    .filter(s -> s.startsWith("L"))
    .map(String::toUpperCase)
    .sorted()
    .collect(toList());

System.out.println(list); 
 [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.
 Stream<String> startsWithT = Stream.of(
   "Monkey", "Lion", "Giraffe", "Lemur"
)

    .filter(s -> s.startsWith("L")); 

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”.
Stream<String> firstTwo = Stream.of(
   "Monkey", "Lion", "Giraffe", "Lemur"
)
   .limit(2);

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 apply skip(2) to our Stream of animals, we are left with the tailing two elements “Giraffe” and “Lemur”.
Stream<String> firstTwo = Stream.of(
   "Monkey", "Lion", "Giraffe", "Lemur"
)
   .skip(2);

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.
Stream<String> uniqueAnimals = Stream.of(
   "Monkey", "Lion", "Giraffe", "Lemur", "Lion"
)
   .distinct();
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.
Stream<String> alphabeticOrder = Stream.of(
    "Monkey", "Lion", "Giraffe", "Lemur"
)
   .sorted();
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:
Stream<String> lengthOrder = Stream.of(
    "Monkey", "Lion", "Giraffe", "Lemur"
)
   .sorted(Comparator.comparing(String::length));
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.
Stream<String> lowerCase = Stream.of(
    "Monkey", "Lion", "Giraffe", "Lemur"
)
   .map(String::toLowerCase);
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 and long.
.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:
IntStream lengths = Stream.of(
    "Monkey", "Lion", "Giraffe", "Lemur"
)
   .mapToInt(String::length);
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.
Stream<Character> chars = Stream.of(
    "Monkey", "Lion", "Giraffe", "Lemur"
)
    .flatMap(s -> s.chars().mapToObj(i -> (char) i));
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.
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

Resources

Become a Master of Java Streams - Part 1: Creating Streams
GitHub Repository "hol-streams"
Speedment Stream ORM Initializer

1 comment:

  1. all your articles are super.. thank you! hope you keep coming up with new articles !

    ReplyDelete

Note: Only a member of this blog may post a comment.