Java 8 Iterable.forEach() vs foreach loop

前端 未结 8 1149
暗喜
暗喜 2020-11-22 14:36

Which of the following is better practice in Java 8?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join));

Java 7:



        
相关标签:
8条回答
  • 2020-11-22 15:25

    When reading this question one can get the impression, that Iterable#forEach in combination with lambda expressions is a shortcut/replacement for writing a traditional for-each loop. This is simply not true. This code from the OP:

    joins.forEach(join -> mIrc.join(mSession, join));
    

    is not intended as a shortcut for writing

    for (String join : joins) {
        mIrc.join(mSession, join);
    }
    

    and should certainly not be used in this way. Instead it is intended as a shortcut (although it is not exactly the same) for writing

    joins.forEach(new Consumer<T>() {
        @Override
        public void accept(T join) {
            mIrc.join(mSession, join);
        }
    });
    

    And it is as a replacement for the following Java 7 code:

    final Consumer<T> c = new Consumer<T>() {
        @Override
        public void accept(T join) {
            mIrc.join(mSession, join);
        }
    };
    for (T t : joins) {
        c.accept(t);
    }
    

    Replacing the body of a loop with a functional interface, as in the examples above, makes your code more explicit: You are saying that (1) the body of the loop does not affect the surrounding code and control flow, and (2) the body of the loop may be replaced with a different implementation of the function, without affecting the surrounding code. Not being able to access non final variables of the outer scope is not a deficit of functions/lambdas, it is a feature that distinguishes the semantics of Iterable#forEach from the semantics of a traditional for-each loop. Once one gets used to the syntax of Iterable#forEach, it makes the code more readable, because you immediately get this additional information about the code.

    Traditional for-each loops will certainly stay good practice (to avoid the overused term "best practice") in Java. But this doesn't mean, that Iterable#forEach should be considered bad practice or bad style. It is always good practice, to use the right tool for doing the job, and this includes mixing traditional for-each loops with Iterable#forEach, where it makes sense.

    Since the downsides of Iterable#forEach have already been discussed in this thread, here are some reasons, why you might probably want to use Iterable#forEach:

    • To make your code more explicit: As described above, Iterable#forEach can make your code more explicit and readable in some situations.

    • To make your code more extensible and maintainable: Using a function as the body of a loop allows you to replace this function with different implementations (see Strategy Pattern). You could e.g. easily replace the lambda expression with a method call, that may be overwritten by sub-classes:

      joins.forEach(getJoinStrategy());
      

      Then you could provide default strategies using an enum, that implements the functional interface. This not only makes your code more extensible, it also increases maintainability because it decouples the loop implementation from the loop declaration.

    • To make your code more debuggable: Seperating the loop implementation from the declaration can also make debugging more easy, because you could have a specialized debug implementation, that prints out debug messages, without the need to clutter your main code with if(DEBUG)System.out.println(). The debug implementation could e.g. be a delegate, that decorates the actual function implementation.

    • To optimize performance-critical code: Contrary to some of the assertions in this thread, Iterable#forEach does already provide better performance than a traditional for-each loop, at least when using ArrayList and running Hotspot in "-client" mode. While this performance boost is small and negligible for most use cases, there are situations, where this extra performance can make a difference. E.g. library maintainers will certainly want to evaluate, if some of their existing loop implementations should be replaced with Iterable#forEach.

      To back this statement up with facts, I have done some micro-benchmarks with Caliper. Here is the test code (latest Caliper from git is needed):

      @VmOptions("-server")
      public class Java8IterationBenchmarks {
      
          public static class TestObject {
              public int result;
          }
      
          public @Param({"100", "10000"}) int elementCount;
      
          ArrayList<TestObject> list;
          TestObject[] array;
      
          @BeforeExperiment
          public void setup(){
              list = new ArrayList<>(elementCount);
              for (int i = 0; i < elementCount; i++) {
                  list.add(new TestObject());
              }
              array = list.toArray(new TestObject[list.size()]);
          }
      
          @Benchmark
          public void timeTraditionalForEach(int reps){
              for (int i = 0; i < reps; i++) {
                  for (TestObject t : list) {
                      t.result++;
                  }
              }
              return;
          }
      
          @Benchmark
          public void timeForEachAnonymousClass(int reps){
              for (int i = 0; i < reps; i++) {
                  list.forEach(new Consumer<TestObject>() {
                      @Override
                      public void accept(TestObject t) {
                          t.result++;
                      }
                  });
              }
              return;
          }
      
          @Benchmark
          public void timeForEachLambda(int reps){
              for (int i = 0; i < reps; i++) {
                  list.forEach(t -> t.result++);
              }
              return;
          }
      
          @Benchmark
          public void timeForEachOverArray(int reps){
              for (int i = 0; i < reps; i++) {
                  for (TestObject t : array) {
                      t.result++;
                  }
              }
          }
      }
      

      And here are the results:

      • Results for -client
      • Results for -server

      When running with "-client", Iterable#forEach outperforms the traditional for loop over an ArrayList, but is still slower than directly iterating over an array. When running with "-server", the performance of all approaches is about the same.

    • To provide optional support for parallel execution: It has already been said here, that the possibility to execute the functional interface of Iterable#forEach in parallel using streams, is certainly an important aspect. Since Collection#parallelStream() does not guarantee, that the loop is actually executed in parallel, one must consider this an optional feature. By iterating over your list with list.parallelStream().forEach(...);, you explicitly say: This loop supports parallel execution, but it does not depend on it. Again, this is a feature and not a deficit!

      By moving the decision for parallel execution away from your actual loop implementation, you allow optional optimization of your code, without affecting the code itself, which is a good thing. Also, if the default parallel stream implementation does not fit your needs, no one is preventing you from providing your own implementation. You could e.g. provide an optimized collection depending on the underlying operating system, on the size of the collection, on the number of cores, and on some preference settings:

      public abstract class MyOptimizedCollection<E> implements Collection<E>{
          private enum OperatingSystem{
              LINUX, WINDOWS, ANDROID
          }
          private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
          private int numberOfCores = Runtime.getRuntime().availableProcessors();
          private Collection<E> delegate;
      
          @Override
          public Stream<E> parallelStream() {
              if (!System.getProperty("parallelSupport").equals("true")) {
                  return this.delegate.stream();
              }
              switch (operatingSystem) {
                  case WINDOWS:
                      if (numberOfCores > 3 && delegate.size() > 10000) {
                          return this.delegate.parallelStream();
                      }else{
                          return this.delegate.stream();
                      }
                  case LINUX:
                      return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                  case ANDROID:
                  default:
                      return this.delegate.stream();
              }
          }
      }
      

      The nice thing here is, that your loop implementation doesn't need to know or care about these details.

    0 讨论(0)
  • 2020-11-22 15:28

    I feel that I need to extend my comment a bit...

    About paradigm\style

    That's probably the most notable aspect. FP became popular due to what you can get avoiding side-effects. I won't delve deep into what pros\cons you can get from this, since this is not related to the question.

    However, I will say that the iteration using Iterable.forEach is inspired by FP and rather result of bringing more FP to Java (ironically, I'd say that there is no much use for forEach in pure FP, since it does nothing except introducing side-effects).

    In the end I would say that it is rather a matter of taste\style\paradigm you are currently writing in.

    About parallelism.

    From performance point of view there is no promised notable benefits from using Iterable.forEach over foreach(...).

    According to official docs on Iterable.forEach :

    Performs the given action on the contents of the Iterable, in the order elements occur when iterating, until all elements have been processed or the action throws an exception.

    ... i.e. docs pretty much clear that there will be no implicit parallelism. Adding one would be LSP violation.

    Now, there are "parallell collections" that are promised in Java 8, but to work with those you need to me more explicit and put some extra care to use them (see mschenk74's answer for example).

    BTW: in this case Stream.forEach will be used, and it doesn't guarantee that actual work will be done in parallell (depends on underlying collection).

    UPDATE: might be not that obvious and a little stretched at a glance but there is another facet of style and readability perspective.

    First of all - plain old forloops are plain and old. Everybody already knows them.

    Second, and more important - you probably want to use Iterable.forEach only with one-liner lambdas. If "body" gets heavier - they tend to be not-that readable. You have 2 options from here - use inner classes (yuck) or use plain old forloop. People often gets annoyed when they see the same things (iteratins over collections) being done various vays/styles in the same codebase, and this seems to be the case.

    Again, this might or might not be an issue. Depends on people working on code.

    0 讨论(0)
提交回复
热议问题