Using Stream.map()

Basically, we call Stream.map​(Function<? super T,​? extends R> mapper) to apply the mapper function on each element of the stream. The result is a new Stream. It doesn't modify the source Stream.

Let's assume that we have the following Melon class:

public class Melon {

private String type;
private int weight;

// constructors, getters, setters, equals(),
// hashCode(), toString() omitted for brevity
}

We also need to assume that we have List<Melon>:

List<Melon> melons = Arrays.asList(new Melon("Gac", 2000),
new Melon("Hemi", 1600), new Melon("Gac", 3000),
new Melon("Apollo", 2000), new Melon("Horned", 1700));

Furthermore, we want to extract only the names of the melons in another list, List<String>.

For this task, we can rely on map(), as follows:

List<String> melonNames = melons.stream()
.map(Melon::getType)
.collect(Collectors.toList());

The output will contain the following types of melons:

Gac, Hemi, Gac, Apollo, Horned

The following diagram depicts how map() works for this example:

So, the map() method gets a Stream<Melon> and outputs a Stream<String>. Each Melon passes through the map() method, and this method extracts the melon's type (which is a String) and stores it in another Stream.

Similarly, we can extract the weights of melons. Since weights are integers, the map() method will return a Stream<Integer>:

List<Integer> melonWeights = melons.stream()
.map(Melon::getWeight)
.collect(Collectors.toList());

The output will contain the following weights:

2000, 1600, 3000, 2000, 1700
Beside map(), the Stream class also provides flavors for primitives such as mapToInt(), mapToLong(), and mapToDouble(). These methods return the int primitive specialization of Stream (IntStream), the long primitive specialization of Stream (LongStream) and the double primitive specialization of Stream (StreamDouble).

While map() can map the elements of a Stream to a new Stream via a Function, do not conclude that we can do the following:

List<Melon> lighterMelons = melons.stream()
.map(m -> m.setWeight(m.getWeight() - 500))
.collect(Collectors.toList());

This will not work/compile because the setWeight() method returns void. In order to make it work, we need to return Melon, but this means we have to add some perfunctory code (for example, return):

List<Melon> lighterMelons = melons.stream()
.map(m -> {
m.setWeight(m.getWeight() - 500);

return m;
})
.collect(Collectors.toList());

What do you think about the peek() temptation? Well, peek() stands for look, but don't touch, but it can be used to mutate state, as follows:

List<Melon> lighterMelons = melons.stream()
.peek(m -> m.setWeight(m.getWeight() - 500))
.collect(Collectors.toList());

The output will contain the following melons (this looks good):

Gac(1500g), Hemi(1100g), Gac(2500g), Apollo(1500g), Horned(1200g)

This is more clear than using map(). Calling setWeight() is a clear signal that we plan to mutate state, but the documentation specifies that the Consumer that's passed to peek() should be a non-interfering action (doesn't modify the data source of the stream).

For sequential streams (such as the preceding one), breaking this expectation can be kept under control without side effects; however, for parallel stream pipelines, the problem may become more complicated.

The action may be called at whatever time and in whatever thread the element is made available by the upstream operation, so if the action modifies the shared state, it is responsible for providing the required synchronization.

As a rule of thumb, think twice before using peek() to mutate the state. Also, be aware that this practice is a debate that falls under the bad practice or even anti-pattern umbrella.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.141.75