Avoid Side Effects

 class​ Inventory {
 
  List<Supply> supplies = ​new​ ArrayList<>();
 
 long​ countDifferentKinds() {
  List<String> names = ​new​ ArrayList<>();
 
» Consumer<String> addToNames = name -> names.add(name);
 
  supplies.stream()
  .filter(Supply::isUncontaminated)
  .map(Supply::getName)
  .distinct()
» .forEach(addToNames);
 return​ names.size();
  }
 }

In theory, there are no side effects with functional programming. Everything’s just a function that takes data as input and produces new data as output. The data you pass along is immutable.

But in imperative and object-oriented programming, we rely on side effects (we change data and state through procedures or methods) all the time. In Java, we can now mix all these styles. This is very powerful, but it’s also quite error prone. That’s why you should try to minimize side effects in your code.

Take a look at the code above. It even makes heavy use of side effects to achieve its goal.

The problem is with the Consumer addToNames that’s called in the forEach() part of the stream. This Consumer adds an element to a list outside of the lambda expression. That’s the side effect.

There’s no functional error in the code above, but it’s prone to break once you add concurrency. Java makes no guarantees regarding the visibility of side effects among different threads. And already if you parallelize the lambda expression here, it will (occasionally) produce wrong results because the ArrayList is not thread-safe.

Newcomers to functional programming in Java have a tendency to write such code. Whereas the filter() and map() operators only act on the stream elements themselves and don’t cause any side effects, beginners often fall back to the imperative style to terminate the stream—and this can only be done through side effects.

So how can we avoid side effects and terminate the lambda expression in a better way?

 class​ Inventory {
 
  List<Supply> supplies = ​new​ ArrayList<>();
 
 long​ countDifferentKinds() {
  List<String> names = supplies.stream()
  .filter(Supply::isUncontaminated)
  .map(Supply::getName)
  .distinct()
» .collect(Collectors.toList());
 return​ names.size();
  }
 }

The crucial part is the list that results from the lambda expression. Instead of populating this list ourselves, we collect() every remaining element in the stream in a collection. For a list, you’ll need to end the stream with collect(Collectors.toList()). Of course, you can get a Set or other data structures with the Collectors.toSet() as well.

In the example above, we still need to get the size of the resulting list for our job at hand. Let’s see if we can do better:

 return​ supplies.stream()
  .filter(Supply::isUncontaminated)
  .map(Supply::getName)
  .distinct()
» .count();

Yes, we can. The terminating operator count() returns the number of remaining elements in the stream—exactly what we need. It’s a shorthand for the reduce operator of the Stream class[46]: reduce(0, (currentResult, streamElement) -> currentResult + 1). This reduces the list to a single integer value. The zero here is the initial value, and we add 1 to the result for every element in the stream.

To sum up, you should try to avoid forEach() for terminating a stream, because it can easily cause side effects. Try to use collect() and reduce() instead, which we’ll explain more closely in Use Collect for Terminating Complex Streams. These operators terminate a stream directly and produce the data structure you need, be it a List, Set, or even a long. This makes your lambda expressions less error prone.

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

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