Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Side effects of lambda expression in java 8

Please consider the below code snippet.

List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
List<String> copyList = new ArrayList<String>(); 
Consumer<String> consumer = s->copyList.add(s);
list.stream().forEach(consumer);

Since we are using lambda expression, as per functional programming (pure functions) it should only compute the input & provide corresponding output.

But here in the example it is trying to add elements to the list which is neither input nor declared inside the lambda scope.

Is this a good practice, I mean, leading to any side effects?

like image 788
Anoop Deshpande Avatar asked Dec 13 '22 15:12

Anoop Deshpande


2 Answers

forEach would be useless if it didn't produce side-effects, since it has no return value. Hence, whenever you use forEach you should be expecting side-effects to take place. Therefore there's nothing wrong with your example.

A Consumer<String> can print the String, or insert it into some database, or write it into some output file, or store it in some Collection (as in your example), etc...

From the Stream Javadoc:

A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which transform a stream into another stream, such as Stream.filter(Predicate)), and a terminal operation (which produces a result or side-effect, such as Stream.count() or Stream.forEach(Consumer)).

Besides, if you look at the Javadoc of Consumer, you'll see that it's expected to have side-effects:

java.util.function.Consumer

Represents an operation that accepts a single input argument and returns no result. Unlike most other functional interfaces, Consumer is expected to operate via side-effects.

I guess this means Java Streams and functional interfaces were not designed to be used only for "purely" functional programming.

like image 71
Eran Avatar answered Dec 29 '22 02:12

Eran


For a forEach or even a stream().forEach it is pretty straightforward. Your example works fine.

Be aware though that if you would do this with other streaming methods, then you could get some surprises: e.g. The following code prints absolutely nothing.

List<String> lst = Arrays.asList("a", "b", "c");

lst.stream().map(
 s -> {
   System.out.println(s);
   return "-";
 });

In this case, the stream acts more like a builder which prepares a process but does not execute it yet. It's only when a collect, count or find... method is called that the lambda is executed.

An easy way to spot this, is by looking at the return type of the map method, which in turn is again a Stream.

Having said that, I think for your specific example, there are easier alternatives.

List<String> list = Arrays.asList("A", "B", "C");

// this is the base pattern for transforming one list to another.
// the map applies a transformation.
List<String> copyList1 = list.stream().map(e -> e).collect(Collectors.toList());

// if it's a 1-to-1 mapping, then you don't really need the map.
List<String> copyList2 = list.stream().collect(Collectors.toList());

// in essence, you could of course just clone the list without streaming.
List<String> copyList3 = new ArrayList<>(list);
like image 37
bvdb Avatar answered Dec 29 '22 02:12

bvdb