for-loop map - Java 8 Iterable.forEach()vs foreach loop




arraylist consumer (8)

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

Sentí que debería agregar mis resultados de la iteración de evaluación comparativa. Tomé un enfoque muy simple (sin marcos de referencia) y evalué 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<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 en esta clase se doIt(Integer i) y se doIt(Integer i) a todos sus miembros, cada vez a través de un método diferente. en la clase Principal, ejecuté 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, 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);

Corrí esto en una CPU i5 4 core, con la versión 1.8.0_05 de Java

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

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

EDITAR

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


Una de las limitaciones más importantes de forEach funcional es la falta de compatibilidad con excepciones comprobadas.

Una posible solución es reemplazar el terminal forEach con un bucle foreach antiguo y simple:

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

Aquí está la lista de las preguntas más populares con otras soluciones en el manejo de excepciones verificadas dentro de lambdas y flujos:

¿Función de Java 8 Lambda que lanza excepción?

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

¿Cómo puedo lanzar excepciones COMPROBADAS desde flujos de Java 8?

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


La ventaja viene 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 bucle se puede definir sin tener que decidir si se ejecutará en paralelo o secuencial.

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

     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 se unen implementando la interfaz java.util.Stream . Si las combinaciones solo implementan la interfaz java.util.Iterable , esto ya no es cierto.


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 efectos secundarios. No profundizaré en las ventajas que puede obtener de esto, ya que esto no está relacionado con la pregunta.

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

Al final, diría que es más bien una cuestión de gustos \ estilo \ paradigma en el que está escribiendo actualmente.

Sobre el paralelismo.

Desde el punto de vista del rendimiento, no hay beneficios notables prometidos al usar Iterable.forEach over foreach (...).

Según los documentos oficiales en Iterable.forEach :

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

... es decir, documenta bastante claro que no habrá un 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 me sean más explícitas y poner un poco más de cuidado al 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 colección subyacente).

ACTUALIZACIÓN: puede que no sea tan obvio y un poco estirado de un vistazo, pero hay otra faceta del estilo y la perspectiva de la legibilidad.

En primer lugar, los forloops antiguos son simples y antiguos. Todos ya los conocen.

Segundo, y más importante: probablemente desee utilizar Iterable.forEach solo con lambdas de una sola línea. Si el "cuerpo" se vuelve más pesado (tienden a no serlo) se puede leer. Tienes 2 opciones desde aquí: usa clases internas (asa) o usa forloop antiguo. La gente a menudo se molesta cuando ve que las mismas cosas (iteratinas sobre colecciones) se realizan en varios estilos / estilos en el mismo código base, y este parece ser el caso.

Una vez más, esto podría o no ser un problema. Depende de las personas que trabajen en el código.


La ventaja de Java 1.8 forEach method over 1.7 Enhanced for loop es que, al escribir código, puede centrarse únicamente en la lógica de negocios.

El método forEach toma el objeto java.util.function.Consumer como un argumento, por lo que ayuda a tener nuestra lógica de negocios en una ubicación separada que puede reutilizarla en cualquier momento.

Echa un vistazo al siguiente fragmento,

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

    class MyConsumer implements Consumer<Integer>{
    
        @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<Integer> 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<Integer> 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<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
    

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

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

No es un atajo para escribir.

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

y ciertamente no debe ser usado de esta manera. En su lugar, está pensado como un acceso directo (aunque no es exactamente el mismo) para escribir

joins.forEach(new Consumer<T>() {
    @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<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 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 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#forEach de la semántica de un bucle tradicional para cada bucle. Una vez que uno se acostumbra a la sintaxis de Iterable#forEach , hace que el código sea más legible, porque de inmediato obtiene esta información adicional sobre el código.

Sin duda, los bucles tradicionales para cada uno seguirán siendo una buena práctica (para evitar el término usado en exceso como " mejor práctica ") en Java. Pero esto no significa que Iterable#forEach deba considerarse 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 los 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 usar Iterable#forEach :

  • Para hacer que su código sea más explícito: como se describe 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 mantenible: el uso de una función como el cuerpo de un bucle le permite reemplazar esta función con diferentes implementaciones (consulte Patrón de estrategia ). Podría, por ejemplo, reemplazar fácilmente la expresión lambda con una llamada de método, que puede ser sobrescrita 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 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, ya que podría tener una implementación de depuración especializada, que imprime los mensajes de depuración, sin la necesidad de saturar su código principal con el sistema if(DEBUG)System.out.println() . La implementación de depuración podría, por ejemplo, ser un delegate , que decorates 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 tradicional para cada bucle, al menos cuando se usa ArrayList y se ejecuta Hotspot en el modo "cliente". Si bien este aumento de rendimiento es pequeño e insignificante para 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 bucle existentes deberían reemplazarse con Iterable#forEach .

    Para respaldar esta afirmación con hechos, he hecho algunos micro-puntos de referencia 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:

    Cuando se ejecuta con "-client", Iterable#forEach supera el bucle for tradicional sobre un 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.

  • 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. Como Collection#parallelStream() no garantiza que el bucle se ejecute en paralelo, se debe considerar que esta es una característica opcional . Al iterar sobre su lista con list.parallelStream().forEach(...); , usted dice explícitamente: Este bucle admite la ejecución paralela, pero no depende de él. Una vez más, 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 que es bueno. Además, si la implementación de la transmisión paralela predeterminada no se ajusta a sus necesidades, nadie le impide proporcionar su propia implementación. Podría, por ejemplo, proporcionar una colección optimizada en función del sistema operativo subyacente, del tamaño de la colección, del número de núcleos y de 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 su implementación de bucle no necesita saber ni preocuparse por estos detalles.


La mejor práctica es utilizar for-each . Además de violar el principio Keep It Simple, Stupid , the 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 forEach lambda:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • No se pueden manejar las excepciones comprobadas . En realidad, a Lambdas no se le prohíbe lanzar excepciones comprobadas, pero las interfaces funcionales comunes como Consumer no declaran ninguna. Por lo tanto, cualquier código que arroje excepciones comprobadas debe envolverlas en try-catch o Throwables.propagate() . Pero incluso si lo hace, no siempre queda claro qué sucede con la excepción lanzada. Podría tragarse en algún lugar de las entrañas de forEach()

  • Control de flujo limitado . Un return en una lambda equivale a una continue en una para cada uno, pero no hay equivalente a una break . También es difícil hacer cosas como valores de retorno, cortocircuito o establecer indicadores (que habrían aliviado un poco las cosas, si no fuera una violación de la regla de las variables no definitivas ). "Esta no es solo una optimización, sino crítica cuando consideras que algunas secuencias (como leer las líneas en un archivo) pueden tener efectos secundarios, o puedes tener una secuencia infinita".

  • Puede ejecutarse en paralelo , lo cual es una cosa horrible, horrible para todos menos el 0.1% de su código que necesita ser optimizado. Debe pensarse en cualquier código paralelo (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 dañar el rendimiento , porque el JIT no puede optimizar forEach () + lambda en la misma medida que los bucles planos, 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 sofisticado análisis y transformación que el compilador JIT moderno realiza en el código en ejecución.

  • Si necesita paralelismo, es probable que sea mucho más rápido y no mucho más difícil usar un ExecutorService . Las transmisiones son automáticas (lea: no sé mucho sobre su problema) y utilice una estrategia de paralelización especializada (lectura: 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 permita, la ejecución paralela. El depurador puede tener problemas para mostrar las variables del código circundante y es posible que cosas 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 de las API complejas " fluent " en general. La combinación de declaraciones simples 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 al "lugar en el que arruinó los tipos, pero no sabemos dónde ni cómo". De manera similar, no puede avanzar 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, la lectura del código y la comprensión de los tipos y el comportamiento en cada etapa de la ejecución pueden no ser triviales.

  • Se pega como un pulgar adolorido . El lenguaje Java ya tiene la sentencia for-each. ¿Por qué reemplazarlo con una llamada de función? ¿Por qué animar a ocultar los efectos secundarios en algún lugar en las expresiones? ¿Por qué animar a los de una línea de difícil manejo? La mezcla regular para cada uno y el nuevo forEach willy-nilly es un estilo malo. El código debe hablar en expresiones idiomáticas (patrones que son rápidos de comprender debido a su repetición), y cuantas menos expresiones se utilicen, más claro será el código y se dedicará menos tiempo a decidir qué idioma usar (¡una gran pérdida de tiempo para perfeccionistas como yo! ).

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

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

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

  • Atómicamente iterando sobre una lista sincronizada . Antes de esto, una lista generada con Collections.synchronizedList() era atómica con respecto a cosas como obtener o establecer, 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 comparación con el uso de un ExecutorService, si su problema coincide con las suposiciones de rendimiento integradas 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 parte teórico a menos que las personas puedan mostrar más ejemplos)

  • Llamar a una sola 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 en las excepciones comprobadas, la depuración más difícil y la reducción de la cantidad de expresiones idiomáticas que utiliza al escribir código.

Artículos que utilicé para referencia:

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


Este es bueno porque:

  • Seguridad de las manos el conjunto de caracteres.
  • Usted controla el tamaño del buffer de lectura.
  • Puede aprovisionar la longitud del constructor y no puede ser exactamente.
  • Está libre de dependencias de la biblioteca.
  • Es para Java 7 o superior.

¿Para qué?

public static String convertStreamToString(InputStream is) {
   if (is == null) return null;
   StringBuilder sb = new StringBuilder(2048); // Define a size if you have an idea of it.
   char[] read = new char[128]; // Your buffer size.
   try (InputStreamReader ir = new InputStreamReader(is, StandardCharsets.UTF_8)) {
     for (int i; -1 != (i = ir.read(read)); sb.append(read, 0, i));
   } catch (Throwable t) {}
   return sb.toString();
}




java for-loop java-8 java-stream