Java 8 Iterable.forEach() vs bucle foreach

Resuelto nebkat asked hace 11 años • 8 respuestas

¿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 podrían "simplificarse" con lambdas, pero ¿hay realmente alguna ventaja en usarlos? ¿Mejoraría su rendimiento y legibilidad?

EDITAR

También extenderé esta pregunta a métodos más largos. Sé que no se puede devolver o interrumpir la función principal de una lambda y esto también debe tenerse en cuenta al compararlas, pero ¿hay algo más que deba considerarse?

nebkat avatar May 19 '13 20:05 nebkat
Aceptado

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

  • No se pueden utilizar variables no finales . Por lo tanto, código como el siguiente no se puede convertir en una lambda forEach:
Object prev = null;
for(Object curr : list)
{
    if( prev != null )
        foo(prev, curr);
    prev = curr;
}
  • No se pueden manejar excepciones marcadas . En realidad, las lambdas no tienen prohibido lanzar excepciones marcadas, pero las interfaces funcionales comunes como Consumerno declaran ninguna. Por lo tanto, cualquier código que arroje excepciones comprobadas debe incluirlas en try-catcho Throwables.propagate(). Pero incluso si haces eso, no siempre está claro qué sucede con la excepción lanzada. Podría ser tragado en algún lugar de las entrañas deforEach()

  • Control de flujo limitado . A returnen una lambda es igual a continuea en for-each, pero no existe un equivalente a a break. También es difícil hacer cosas como valores de retorno, cortocircuitos o establecer indicadores (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 sólo una optimización, sino que es fundamental si se tiene en cuenta que algunas secuencias (como leer las líneas de un archivo) pueden tener efectos secundarios o puede tener una secuencia infinita".

  • Podría ejecutarse en paralelo , lo cual es algo horrible, horrible para todos excepto para el 0,1% del código que necesita optimizarse. Cualquier código paralelo debe ser pensado detenidamente (incluso si no utiliza bloqueos, volátiles y otros aspectos particularmente desagradables de la ejecución tradicional de subprocesos múltiples). Cualquier error será difícil de encontrar.

  • Podría perjudicar el rendimiento , porque JIT no puede optimizar forEach()+lambda en la misma medida que los bucles simples, especialmente ahora que las lambdas son nuevas. Por "optimización" no me refiero a la sobrecarga de llamar a lambdas (que es pequeña), sino al análisis y la transformación sofisticados que realiza el compilador JIT moderno al ejecutar el código.

  • Si necesita paralelismo, probablemente sea mucho más rápido y no mucho más difícil usar un ExecutorService . Las transmisiones son automáticas (léase: no sé mucho sobre su problema) y utilizan una estrategia de paralelización especializada (léase: ineficiente 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 quiera, la ejecución paralela. El depurador puede tener problemas para mostrar variables del código circundante y es posible que elementos como el paso a paso no funcionen como se esperaba.

  • Las transmisiones en general son más difíciles de codificar, leer y depurar . En realidad, esto es cierto para las API complejas " fluidas " en general. La combinación de declaraciones únicas complejas, el uso intensivo de genéricos 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 confundiste los tipos, pero no sabemos dónde ni cómo". De manera similar, no se pueden recorrer y examinar cosas en un depurador tan fácilmente como cuando el código se divide en varias declaraciones y los valores intermedios se guardan en variables. Finalmente, leer el código y comprender los tipos y el comportamiento en cada etapa de ejecución puede no ser trivial.

  • Sobresale como un pulgar dolorido . El lenguaje Java ya tiene la declaración for-each. ¿Por qué reemplazarlo con una llamada a función? ¿Por qué fomentar que se oculten los efectos secundarios en alguna parte de las expresiones? ¿Por qué fomentar frases ingeniosas y difíciles de manejar? Mezclar for-each regular y forEach nuevo, quiera o no, es un mal estilo. El código debe hablar en modismos (patrones que se comprenden rápidamente debido a su repetición), y cuantos menos modismos se utilicen, más claro será el código y se dedicará menos tiempo a decidir qué modismo usar (¡una gran pérdida de tiempo para perfeccionistas como yo! ).

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

Particularmente ofensivo para mí es el hecho de que Streamno se implementa Iterable(a pesar de tener el método iterator) y no se puede usar en un for-each, solo con un forEach(). Recomiendo convertir Streams en Iterables con (Iterable<T>)stream::iterator. Una mejor alternativa es utilizar StreamEx , que soluciona una serie de problemas de Stream API, incluida la implementación Iterable.

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

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

  • Ejecución paralela (utilizando un flujo paralelo apropiado) . Esto le ahorra algunas líneas de código en lugar de usar un ExecutorService, si su problema coincide con los supuestos de rendimiento integrados en Streams y Spliterators.

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

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

Artículos que utilicé como referencia:

  • Todo sobre Java 8
  • Iteración por dentro y por fuera (como lo señala otro cartel)

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

Aleksandr Dubinsky avatar Nov 24 '2013 16:11 Aleksandr Dubinsky

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 se puede definir la implementación de lo que se va a hacer dentro del bucle sin tener que decidir si se ejecutará en paralelo o secuencial.

  • Si desea que su bucle se ejecute en paralelo, simplemente puede escribir

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

    Tendrá que escribir algún código adicional para el manejo de subprocesos, etc.

Nota: Para mi respuesta, asumí que las uniones implementan la java.util.Streaminterfaz. Si joins implementa solo la java.util.Iterableinterfaz, esto ya no es cierto.

mschenk74 avatar May 19 '2013 14:05 mschenk74

Al leer esta pregunta, uno puede tener la impresión de que, Iterable#forEachen combinación con expresiones lambda, es un atajo/reemplazo para escribir un bucle tradicional para cada uno. Esto simplemente no es cierto. 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 debe usarse de esta manera. Más bien pretende ser un atajo (aunque no es exactamente lo mismo) para escribir

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

Y es un reemplazo del siguiente código Java 7:

final Consumer<T> c = new Consumer<T>() {
    @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: está diciendo que (1) el cuerpo del bucle no afecta el código circundante ni 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 variables no finales del alcance externo no es un déficit de funciones/lambdas, es una característica que distingue la semántica de Iterable#forEachla semántica de un bucle for-each tradicional. Una vez que uno se acostumbra a la sintaxis de Iterable#forEach, hace que el código sea más legible, porque inmediatamente obtiene esta información adicional sobre el código.

Los bucles for-each tradicionales ciertamente seguirán siendo una buena práctica (para evitar el término usado en exceso " mejores prácticas ") en Java. Pero esto no significa que Iterable#forEachdeba considerarse una mala práctica o un mal estilo. Siempre es una buena práctica utilizar la herramienta adecuada para hacer el trabajo, y esto incluye mezclar bucles tradicionales para cada uno con Iterable#forEach, cuando tenga sentido.

Dado que las desventajas Iterable#forEachya se han discutido en este hilo, aquí hay algunas razones por las que probablemente quieras usar Iterable#forEach:

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

    joins.forEach(getJoinStrategy());
    

    Luego, podría proporcionar estrategias predeterminadas utilizando una enumeración que implemente la interfaz funcional. Esto no sólo 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 declaración del bucle.

  • Para hacer que su código sea más depurable: separar la implementación del bucle de la declaración también puede hacer que la depuración sea más fácil, porque podría tener una implementación de depuración especializada, que imprima mensajes de depuración, sin la necesidad de saturar su código principal con archivos 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 crítico para el rendimiento: contrariamente a algunas de las afirmaciones en este hilo, Iterable#forEach ya proporciona un mejor rendimiento que un bucle tradicional para cada, al menos cuando se usa ArrayList y se ejecuta Hotspot en modo "-cliente". Si bien este aumento de rendimiento es pequeño e insignificante en la mayoría de los casos de uso, hay situaciones en las que este rendimiento adicional puede marcar la diferencia. Por ejemplo, los mantenedores de bibliotecas seguramente querrán evaluar si algunas de sus implementaciones de bucles existentes deberían reemplazarse por Iterable#forEach.

    Para respaldar esta afirmación con hechos, he realizado algunos microevaluaciones con Caliper . Aquí está el código de prueba (se necesita el último Caliper de git):

    @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++;
                }
            }
        }
    }
    

    Y aquí están los resultados:

    • Resultados para -cliente
    • Resultados para -servidor

    Cuando se ejecuta con "-client", Iterable#forEachsupera al bucle for tradicional sobre una ArrayList, pero sigue siendo más lento que la iteración directa sobre una matriz. Cuando se ejecuta con "-server", el rendimiento de todos los enfoques es aproximadamente el mismo.

  • Proporcionar soporte opcional para la ejecución paralela: Ya se ha dicho aquí que la posibilidad de ejecutar la interfaz funcional Iterable#forEachen paralelo utilizando flujos es sin duda un aspecto importante. Dado que Collection#parallelStream()no garantiza que el bucle se ejecute realmente en paralelo, se debe considerar esto como una característica opcional . Al iterar sobre su lista con list.parallelStream().forEach(...);, dice explícitamente: Este bucle admite la ejecución paralela, pero no depende de ella. Nuevamente, ¡esta es una característica y no un déficit!

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

    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();
            }
        }
    }
    

    Lo bueno aquí es que la implementación de su bucle no necesita conocer ni preocuparse por estos detalles.

Balder avatar Mar 19 '2014 10:03 Balder

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

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

for(int i=0; i<size; i++)
    action.accept(elements[i])

a diferencia del bucle para cada uno que requiere mucho andamiaje

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

Sin embargo, también debemos tener en cuenta dos costos generales mediante el uso de forEach(), uno es crear el objeto lambda y el otro es invocar el método lambda. Probablemente no sean significativos.

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

ZhongYu avatar May 19 '2013 18:05 ZhongYu

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

Sentí que debía agregar los resultados de la iteración de evaluación comparativa. Adopté un enfoque muy simple (sin marcos de evaluación comparativa) y comparé 5 métodos diferentes:

  1. clásicofor
  2. clásico para cada uno
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

El procedimiento de prueba y los parámetros.

private List<Integer> 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 repetirá y se doIt(Integer i)aplicará algo a todos sus miembros, cada vez mediante 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 sumando el tiempo que lleva cada método de iteración (usando System.nanoTime()). Una vez hecho esto, divido esa suma por 1000 y ese es el resultado, 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 de 4 núcleos, con la versión java 1.8.0_05

clásicofor

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

tiempo de ejecución: 4,21 ms

clásico para cada uno

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

Assaf avatar Sep 15 '2014 19:09 Assaf