Java 8 Iterable.forEach () frente a foreach loop

¿Cuál de las siguientes es una mejor práctica en Java 8?

Java 8:

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

Java 7:

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

Tengo muchos bucles for que se pueden “simplificar” con lambdas, pero ¿hay alguna ventaja de usarlos, incluido el rendimiento y la legibilidad?

EDITAR

También ampliaré esta pregunta a métodos más largos: sé que no puedes devolver o romper la función primaria de una lambda y esto debería mencionarse si se comparan, pero ¿hay algo más que deba considerarse?

La ventaja se tiene en cuenta cuando las operaciones se pueden ejecutar en paralelo. (Consulte http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and- la sección sobre iteración interna y externa)

  • La principal ventaja desde mi punto de vista es que la implementación de lo que se debe hacer dentro del ciclo se puede definir sin tener que decidir si se ejecutará en paralelo o secuencialmente.

  • Si quieres que tu loop se ejecute en paralelo, simplemente puedes escribir

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

    Deberá escribir un código adicional para el manejo de hilos, etc.

Nota: Para mi respuesta, asumí que se une la implementación de la interfaz java.util.Stream . Si join implementa solo la interfaz java.util.Iterable , esto ya no es cierto.

La mejor práctica es usar for-each . Además de violar el principio Keep It Simple, Stupid , el new- forEach() tiene al menos las siguientes deficiencias:

  • No se pueden usar variables no finales . Por lo tanto, un código como el siguiente no se puede convertir en un para cada lambda:

     Object prev = null; for(Object curr : list) { if( prev != null ) foo(prev, curr); prev = curr; } 
  • No puede manejar excepciones marcadas . Las lambdas no tienen prohibido arrojar excepciones comprobadas, pero las interfaces funcionales comunes como Consumer no las declaran. Por lo tanto, cualquier código que arroje excepciones controladas debe envolverlos en try-catch o Throwables.propagate() . Pero incluso si haces eso, no siempre queda claro qué sucede con la excepción lanzada. Podría ser tragado en algún lugar en las entrañas de forEach()

  • Control de flujo limitado . Un return en un lambda equivale a continue en un for-each, pero no hay equivalente a un break . También es difícil hacer cosas como valores de retorno, cortocircuito o establecer banderas (lo que habría aliviado un poco las cosas, si no fuera una violación de la regla de no variables no finales ). “Esto no es solo una optimización, sino fundamental cuando se considera que algunas secuencias (como leer las líneas en un archivo) pueden tener efectos secundarios, o puede tener una secuencia infinita”.

  • Podría ejecutarse en paralelo , lo que es horrible, horrible para todos, pero el 0.1% de su código necesita ser optimizado. Cualquier código paralelo debe ser pensado (incluso si no usa lockings, volátiles y otros aspectos particularmente desagradables de la ejecución tradicional de múltiples subprocesos). Cualquier error será difícil de encontrar.

  • Puede dañar el rendimiento , porque el JIT no puede optimizar para cada () + lambda en la misma medida que los bucles normales, especialmente ahora que las lambdas son nuevas. Por “optimización” no me refiero a la sobrecarga de llamar a lambdas (que es pequeño), sino al análisis sofisticado y la transformación que el comstackdor JIT moderno realiza en el código en ejecución.

  • Si necesita un paralelismo, probablemente sea mucho más rápido y no sea mucho más difícil usar un ExecutorService . Los flujos son automágicos (léase: no sabe mucho sobre su problema) y utiliza una estrategia de paralelización especializada (léase: ineficaz para el caso general) ( descomposición recursiva fork-join ).

  • Hace que la depuración sea más confusa , debido a la jerarquía de llamadas anidadas y, Dios no lo permita, a la ejecución paralela. El depurador puede tener problemas al mostrar variables del código circundante, y cosas como el paso a través pueden no funcionar como se esperaba.

  • Los flujos en general son más difíciles de codificar, leer y depurar . En realidad, esto es cierto para las API complejas y ” fluidas ” en general. La combinación de declaraciones simples complejas, el uso intensivo de generics y la falta de variables intermedias conspiran para producir mensajes de error confusos y frustrar la depuración. En lugar de “este método no tiene una sobrecarga para el tipo X”, aparece un mensaje de error más cercano a “en algún lugar en el que confundió los tipos, pero no sabemos dónde ni cómo”. Del mismo modo, no puede pasar y examinar cosas en un depurador tan fácilmente como cuando el código se divide en varias instrucciones y los valores intermedios se guardan en variables. Finalmente, leer el código y comprender los tipos y el comportamiento en cada etapa de la ejecución puede ser no trivial.

  • Se destaca como un pulgar dolorido . El lenguaje Java ya tiene el enunciado for-each. ¿Por qué reemplazarlo con una llamada a función? ¿Por qué fomentar la ocultación de efectos secundarios en algún lugar de las expresiones? ¿Por qué alentar a los intransigentes? Mezclar para cada uno y nuevo para cada uno de forma regular es un mal estilo. El código debe hablar en modismos (patrones que son rápidos de comprender debido a su repetición), y cuantas menos expresiones idiomáticas se usen, más claro es el código y menos tiempo se dedica a decidir qué idioma usar (¡una gran pérdida de tiempo para los perfeccionistas como yo! )

Como puede ver, no soy un gran fanático de forEach () excepto en los casos en los que tiene sentido.

Particularmente ofensivo para mí es el hecho de que Stream no implementa Iterable (a pesar de que en realidad tiene iterator método) y no se puede usar en un for-each, solo con un forEach (). Recomiendo (Iterable)stream::iterator flujos en Iterables con (Iterable)stream::iterator . Una mejor alternativa es usar StreamEx, que soluciona varios problemas de Stream API, incluida la implementación de Iterable .

Dicho esto, forEach() es útil para lo siguiente:

  • Iteración atómica sobre una lista sincronizada . Antes de esto, una lista generada con Collections.synchronizedList() era atómica con respecto a cosas como get o set, pero no era segura para subprocesos al iterar.

  • Ejecución en paralelo (usando una secuencia paralela apropiada) . Esto le ahorra unas pocas líneas de código frente a usar un ExecutorService, si su problema coincide con las suposiciones de rendimiento integradas en Streams y Spliterators.

  • Contenedores específicos que , al igual que la lista sincronizada, se benefician de tener el control de la iteración (aunque esto es en gran parte teórico a menos que las personas puedan traer más ejemplos)

  • Llamar a una única función de forma más limpia utilizando forEach() y un argumento de referencia de método (es decir, list.forEach (obj::someMethod) ). Sin embargo, tenga en cuenta los puntos sobre las excepciones comprobadas, la depuración más difícil y la reducción del número de modismos que utiliza al escribir el código.

Artículos que utilicé para referencia:

  • Todo sobre Java 8
  • Iteraciones por dentro y por fuera (como lo señala otro afiche)

EDITAR: Parece que algunas de las propuestas originales para lambdas (como http://www.javac.info/closures-v06a.html ) resolvieron algunos de los problemas que mencioné (al tiempo que agregaban sus propias complicaciones, por supuesto).

Al leer esta pregunta, uno puede dar la impresión de que Iterable#forEach en combinación con expresiones lambda es un atajo / reemplazo para escribir un bucle for-each tradicional. Esto simplemente no es verdad. Este código del OP:

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

no pretende ser un atajo para escribir

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

y ciertamente no debería ser usado de esta manera. En cambio, está pensado como un atajo (aunque no es exactamente lo mismo) para escribir

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

Y es como un reemplazo para el siguiente código de Java 7:

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

Reemplazar el cuerpo de un bucle con una interfaz funcional, como en los ejemplos anteriores, hace que su código sea más explícito: usted está diciendo que (1) el cuerpo del bucle no afecta el código circundante y el flujo de control, y (2) el el cuerpo del bucle se puede reemplazar con una implementación diferente de la función, sin afectar el código circundante. No poder acceder a las variables no finales del ámbito externo no es un déficit de funciones / lambdas, es una característica que distingue la semántica de Iterable#forEach de la semántica de un ciclo for-each tradicional. Una vez que uno se acostumbra a la syntax de Iterable#forEach , hace que el código sea más legible, porque inmediatamente obtiene esta información adicional sobre el código.

Los tradicionales bucles for-each serán seguramente una buena práctica (para evitar el término usado en exceso ” mejores prácticas “) en Java. Pero esto no significa que el Iterable#forEach debe considerar una mala práctica o un mal estilo. Siempre es una buena práctica usar la herramienta adecuada para hacer el trabajo, y esto incluye mezclar bucles tradicionales para cada uno con Iterable#forEach , donde tiene sentido.

Dado que las desventajas de Iterable#forEach ya se han discutido en este hilo, aquí hay algunas razones por las que probablemente desee utilizar Iterable#forEach :

  • Para hacer su código más explícito: Como se describió anteriormente, Iterable#forEach puede hacer que su código sea más explícito y legible en algunas situaciones.

  • Para que su código sea más extensible y fácil de mantener: el uso de una función como cuerpo de un bucle le permite reemplazar esta función con diferentes implementaciones (consulte el Patrón de estrategia ). Por ejemplo, podría reemplazar fácilmente la expresión lambda con una llamada a método, que puede sobrescribirse por subclases:

     joins.forEach(getJoinStrategy()); 

    Luego, podría proporcionar estrategias predeterminadas utilizando una enumeración, que implementa la interfaz funcional. Esto no solo hace que su código sea más extensible, sino que también aumenta la capacidad de mantenimiento porque desacopla la implementación del bucle de la statement de bucle.

  • Para hacer que su código sea más depurable: separar la implementación de bucle de la statement también puede facilitar la depuración, ya que podría tener una implementación de depuración especializada, que imprime mensajes de depuración, sin la necesidad de ocupar el código principal con el sistema if(DEBUG)System.out.println() . La implementación de depuración podría ser, por ejemplo, un delegado , que decora la implementación de la función real.

  • Para optimizar el código de rendimiento crítico: Al contrario de algunas de las aserciones en este hilo, Iterable#forEach ya proporciona un mejor rendimiento que un bucle for-each tradicional, al menos cuando se usa ArrayList y se ejecuta Hotspot en el modo “-cliente”. Si bien este aumento en el rendimiento es pequeño e insignificante para la mayoría de los casos de uso, existen situaciones en las que este rendimiento adicional puede marcar la diferencia. Por ejemplo, los mantenedores de bibliotecas ciertamente querrán evaluar, si algunas de sus implementaciones de bucle existentes deben ser reemplazadas por Iterable#forEach .

    Para respaldar esta statement con hechos, he hecho algunos micro-puntos de referencia con Caliper . Aquí está el código de prueba (se necesita la última Caliper de git):

     @VmOptions("-server") public class Java8IterationBenchmarks { public static class TestObject { public int result; } public @Param({"100", "10000"}) int elementCount; ArrayList 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() { @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++; } } } } 

    Y aquí están los resultados:

    • Resultados para -cliente
    • Resultados para -server

    Cuando se ejecuta con "-client", Iterable#forEach supera el bucle for tradicional sobre ArrayList, pero es aún más lento que la iteración directa sobre una matriz. Cuando se ejecuta con "-server", el rendimiento de todos los enfoques es casi el mismo.

  • Para proporcionar soporte opcional para la ejecución en paralelo: Ya se ha dicho aquí, que la posibilidad de ejecutar la interfaz funcional de Iterable#forEach en paralelo utilizando streams , es ciertamente un aspecto importante. Dado que Collection#parallelStream() no garantiza que el ciclo se ejecute en paralelo, se debe considerar esta característica opcional . Al iterar sobre su lista con list.parallelStream().forEach(...); , dices explícitamente: este ciclo admite ejecución paralela, pero no depende de ello. Nuevamente, esta es una característica y no un déficit.

    Al trasladar la decisión para la ejecución paralela lejos de su implementación real del bucle, permite la optimización opcional de su código, sin afectar el código en sí, lo cual es algo bueno. Además, si la implementación de flujo paralelo predeterminada no se ajusta a sus necesidades, nadie le impide proporcionar su propia implementación. Por ejemplo, podría proporcionar una recostackción optimizada según el sistema operativo subyacente, el tamaño de la colección, el número de núcleos y algunas configuraciones de preferencias:

     public abstract class MyOptimizedCollection implements Collection{ private enum OperatingSystem{ LINUX, WINDOWS, ANDROID } private OperatingSystem operatingSystem = OperatingSystem.WINDOWS; private int numberOfCores = Runtime.getRuntime().availableProcessors(); private Collection delegate; @Override public Stream 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(); } } } 

    Lo bueno aquí es que su implementación de bucle no necesita saber o preocuparse por estos detalles.

forEach() se puede implementar para que sea más rápido que para cada bucle, porque el iterable conoce la mejor manera de iterar sus elementos, a diferencia del modo de iterador estándar. Entonces la diferencia es el bucle interno o el bucle externo.

Por ejemplo, ArrayList.forEach(action) se puede implementar simplemente como

 for(int i=0; i 

a diferencia del bucle for-each que requiere una gran cantidad de andamios

 Iterator iter = list.iterator(); while(iter.hasNext()) Object next = iter.next(); do something with `next` 

Sin embargo, también necesitamos contabilizar dos costos generales mediante forEach() , uno está haciendo el objeto lambda, el otro está invocando el método lambda. Probablemente no sean significativos.

ver también http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ para comparar iteraciones internas / externas para diferentes casos de uso.

TL; DR : List.stream().forEach() fue el más rápido.

Sentí que debería agregar mis resultados de la iteración de benchmarking. Tomé un enfoque muy simple (sin marcos de referencia) y comparé 5 métodos diferentes:

  1. clásico for
  2. foreach clásico
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

el procedimiento de prueba y los parámetros

 private List list; private final int size = 1_000_000; public MyClass(){ list = new ArrayList<>(); Random rand = new Random(); for (int i = 0; i < size; ++i) { list.add(rand.nextInt(size * 50)); } } private void doIt(Integer i) { i *= 2; //so it won't get JITed out } 

La lista de esta clase se doIt(Integer i) y se doIt(Integer i) un doIt(Integer i) a todos sus miembros, cada vez a través de un método diferente. en la clase principal, ejecuto el método probado tres veces para calentar la JVM. Luego ejecuto el método de prueba 1000 veces sumndo el tiempo que toma para cada método de iteración (usando System.nanoTime() ). Después de eso, divido esa sum por 1000 y ese es el resultado, el tiempo promedio. ejemplo:

 myClass.fored(); myClass.fored(); myClass.fored(); for (int i = 0; i < reps; ++i) { begin = System.nanoTime(); myClass.fored(); end = System.nanoTime(); nanoSum += end - begin; } System.out.println(nanoSum / reps); 

Ejecuté esto en una CPU i5 4 core, con la versión java 1.8.0_05

clásico for

 for(int i = 0, l = list.size(); i < l; ++i) { doIt(list.get(i)); } 

tiempo de ejecución: 4.21 ms

foreach clásico

 for(Integer i : list) { doIt(i); } 

tiempo de ejecución: 5.95 ms

List.forEach()

 list.forEach((i) -> doIt(i)); 

tiempo de ejecución: 3.11 ms

List.stream().forEach()

 list.stream().forEach((i) -> doIt(i)); 

tiempo de ejecución: 2.79 ms

List.parallelStream().forEach

 list.parallelStream().forEach((i) -> doIt(i)); 

tiempo de ejecución: 3.6 ms

Siento que necesito extender mi comentario un poco …

Sobre paradigma \ estilo

Ese es probablemente el aspecto más notable. FP se hizo popular debido a lo que puede obtener evitando los efectos secundarios. No profundizaré en los pros y contras que puede obtener de esto, ya que esto no está relacionado con la pregunta.

Sin embargo, diré que la iteración usando Iterable.forEach está inspirada en FP y más bien como resultado de traer más FP a Java (irónicamente, diría que no hay mucho uso para forEach en FP pura, ya que no hace nada excepto introducir efectos secundarios).

Al final, diría que es más una cuestión de gusto / estilo / paradigma en la que estás escribiendo actualmente.

Sobre el paralelismo.

Desde el punto de vista del rendimiento, no se han prometido beneficios notables al usar Iterable.forEach sobre foreach (…).

De acuerdo con los documentos oficiales en Iterable.forEach :

Realiza la acción dada en los contenidos de Iterable, en el orden en que ocurren los elementos al iterar, hasta que todos los elementos hayan sido procesados ​​o la acción arroje una excepción.

… es decir, prácticamente está claro que no habrá paralelismo implícito. Agregar uno sería una violación de LSP.

Ahora, hay “colecciones paralelas” que se prometen en Java 8, pero para trabajar con las que necesita me son más explícitas y ponga un cuidado especial para usarlas (consulte la respuesta de mschenk74, por ejemplo).

Por cierto: en este caso, se utilizará Stream.forEach , y no garantiza que el trabajo real se realice en paralelo (depende de la recostackción subyacente).

ACTUALIZACIÓN: puede no ser tan obvio y un poco estirado a simple vista, pero hay otra faceta de la perspectiva de estilo y legibilidad.

Antes que nada, los viejos forloops son sencillos y antiguos. Todos ya los conocen.

En segundo lugar, y más importante, es probable que desee utilizar Iterable.forEach solo con lambdas de una sola línea. Si el “cuerpo” se vuelve más pesado, tienden a no ser tan legibles. Tiene 2 opciones desde aquí: use las clases internas (yuck) o use plain forloop. Las personas a menudo se molestan cuando ven las mismas cosas (iteratins sobre las colecciones) haciendo varios vays / styles en la misma base de código, y este parece ser el caso.

Nuevamente, esto podría o no ser un problema. Depende de las personas que trabajan en el código.

Una de las funciones más forEach para las limitaciones de forEach es la falta de compatibilidad con excepciones controladas.

Una solución posible es reemplazar la terminal para cada forEach con el viejo bucle foreach:

  Stream stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty()); Iterable iterable = stream::iterator; for (String s : iterable) { fileWriter.append(s); } 

Aquí hay una lista de las preguntas más populares con otras soluciones en el manejo de excepciones comprobadas dentro de lambdas y streams:

¿La función Java 8 Lambda arroja una excepción?

Java 8: Lambda-Streams, filtrar por método con excepción

¿Cómo puedo lanzar excepciones CHECKED desde dentro de las secuencias Java 8?

Java 8: control obligatorio de excepciones comprobadas en expresiones lambda. ¿Por qué obligatorio, no opcional?

La ventaja de Java 1.8 para cada método sobre 1.7 Mejorado para el bucle es que al escribir código, puede enfocarse en la lógica de negocios solamente.

para Cada método toma java.util.function.Consumer object como argumento, por lo que ayuda a tener nuestra lógica de negocio en una ubicación separada que puede volver a utilizar en cualquier momento.

Mira el fragmento de abajo,

  • Aquí he creado una clase nueva que anulará el método de clase de aceptación de la clase de consumidor, donde puede agregar funcionalidad adicional, más que iteración … !!!!!!

     class MyConsumer implements Consumer{ @Override public void accept(Integer o) { System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o); } } public class ForEachConsumer { public static void main(String[] args) { // Creating simple ArrayList. ArrayList aList = new ArrayList<>(); for(int i=1;i< =10;i++) aList.add(i); //Calling forEach with customized Iterator. MyConsumer consumer = new MyConsumer(); aList.forEach(consumer); // Using Lambda Expression for Consumer. (Functional Interface) Consumer lambda = (Integer o) ->{ System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o); }; aList.forEach(lambda); // Using Anonymous Inner Class. aList.forEach(new Consumer(){ @Override public void accept(Integer o) { System.out.println("Calling with Anonymous Inner Class "+o); } }); } } 

Here are some excerpts from the famous book, Effective Java (Third Edition) (Items 45 and 46). Indeed I would replace every loop of my code with forEach operation until I read these:

Overusing streams makes programs hard to read and maintain
When you start using streams, you may feel the urge to convert all your loops into streams, but resist the urge. While it may be possible, it will likely harm the readability and maintainability of your code base.

1.

[…] The problem stems from the fact that this code is doing all its work in a terminal forEach operation, using a lambda that mutates external state […]. A forEach operation that does anything more than present the result of the computation performed by a stream is a “bad smell in code,” as is a lambda that mutates state.

Java programmers know how to use for-each loops, and the forEach terminal operation is similar. But the forEach operation is among the least powerful of the terminal operations and the least stream-friendly. It’s explicitly iterative, and hence not amenable to parallelization. The forEach operation should be used only to report the result of a stream computation, not to perform the computation. Occasionally, it makes sense to use forEach for some other purpose, such as adding the results of a stream computation to a preexisting collection.