Search code examples
javafunctional-programmingjava-stream

Java Stream Methods vs foreach


I have a teammate that did all his stream operations in a foreach execution. I tried to come up with advantages of using the filter, map and to list methods instead.

What are other advantages it has other than readability (arguably the foreach is pretty readable as well) Here is a snippet for something close to what he did:

List<Integer> firstInts = new ArrayList<>();
List<Integer> secondInts = new ArrayList<>();
List<Integer> numbersList = IntStream.range(0, max).boxed().toList();


//his stream
numbersList.stream()
       .forEach(i -> {
           if(i % 6 != 0) {
               return;
           }
           secondInts.add(i);
       });


//alternative 1
numbersList.stream()
        .filter(i -> i % 6 == 0)
        .forEach(firstInts::add);


//alternative 2
List<Integer> third = numbersList.stream()
        .filter(i -> i % 6 == 0)
        .toList();

what is the motivation of using stream methods other than readability?


Solution

  • Alternative 2 looks best to me in this case

    1. Readability

    • Alternative 2 has less number of lines
    • Alternative 2 read more close to return a list containing number divisible by 6 from numList, while forEach approach means add number divisible by 6 from numList to secondInts
    • filter(i -> i % 6 == 0) is straight forward and
      if(i % 6 != 0) {
          return;
      }
      
      require some time for human brain to process.

    2. Performance

    From Stream.toList()

    Implementation Note: Most instances of Stream will override this method and provide an implementation that is highly optimized compared to the implementation in this interface.

    We benefit from optimization from JDK by using Stream API.

    And in this case, using forEach and adding element one by one will be slower, especially when the list is large. It is because ArrayList will need to extend it capacity whenever the list full, while Stream implementation ImmutableCollections.listFromTrustedArrayNullsAllowed just store the result array into ListN.

    One more point to note about parallelism:
    From Stream#forEach

    The behavior of this operation is explicitly nondeterministic. For parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism. For any given element, the action may be performed at whatever time and in whatever thread the library chooses. If the action accesses shared state, it is responsible for providing the required synchronization.

    numbersList.stream().parallel()
           .forEach(i -> {
               if(i % 6 != 0) {
                   return;
               }
               secondInts.add(i);
           });
    

    Will provide unexpected result, while

    List<Integer> third = numbersList.stream().parallel()
          .filter(i -> i % 6 == 0).sorted().forEach()
          .toList();
    

    is totally fine.

    3. Flexibility

    Imagine you want the filtered list to be sorted, in forEach approach, you can do it like:

    numbersList.stream().sorted().
           .forEach(i -> {
               if(i % 6 != 0) {
                   return;
               }
               secondInts.add(i);
           });
    

    Which is much slower compared to

    numbersList.stream()
            .filter(i -> i % 6 == 0)
            .sorted()
            .toList();
    

    As we need to sort the whole numbersList instead of filtered.

    Or if you want to limit your result to 10 elements, it is not straight forward to do so with forEach, but just as simple as adding limit(10) when using stream.

    4. Less error prone

    Stream API usually return Immutable object by default.

    From Stream.toList()

    Implementation Requirements: The implementation in this interface returns a List produced as if by the following: Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())))

    Meaning that the returned list is immutable by default. Some advantages of immutability are:

    1. You can safely pass the list around to different method without worrying the list is modified.
    2. Immutable list are thread safe.

    Read Pros. / Cons. of Immutability vs. Mutability for further discussion.