Java 8 Iterable.forEach () vs foreach loop

466

¿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 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 romper la función principal de un lambda y esto también debe tenerse en cuenta al compararlos, pero ¿hay algo más que considerar?

nebkat
fuente
10
No hay una ventaja real de rendimiento de uno sobre otro. La primera opción es algo inspirado en FP (de quien comúnmente se habla como una forma más "agradable" y "clara" de expresar su código). En realidad, esta es una pregunta de "estilo".
Eugene Loy
55
@Dwb: en este caso, eso no es relevante. forEach no se define como paralelo o algo así, por lo que estas dos cosas son semánticamente equivalentes. Por supuesto, es posible implementar una versión paralela de forEach (y una ya podría estar presente en la biblioteca estándar), y en tal caso la sintaxis de expresión lambda sería muy útil.
AardvarkSoup
8
@AardvarkSoup La instancia en la que se llama forEach es un Stream ( lambdadoc.net/api/java/util/stream/Stream.html ). Para solicitar una ejecución paralela, se podría escribir joins.parallel (). ForEach (...)
mschenk74
13
¿Es joins.forEach((join) -> mIrc.join(mSession, join));realmente una "simplificación" de for (String join : joins) { mIrc.join(mSession, join); }? Has aumentado el recuento de puntuación de 9 a 12, en beneficio de ocultar el tipo de join. Lo que realmente has hecho es poner dos declaraciones en una línea.
Tom Hawtin - tackline
77
Otro punto a considerar es la capacidad limitada de captura variable de Java. Con Stream.forEach (), no puede actualizar las variables locales ya que su captura las hace finales, lo que significa que puede tener un comportamiento con estado en forEach lambda (a menos que esté preparado para alguna fealdad, como el uso de variables de estado de clase).
RedGlyph

Respuestas:

512

La mejor práctica es usar for-each. Además de violar el principio estúpido Keep It Simple, el nuevo 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 lambda forEach:

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

  • Control de flujo limitado . A returnen una lambda es igual a a continueen for-each, pero no hay equivalente a a break. También es difícil hacer cosas como valores de retorno, cortocircuito o establecer indicadores (lo que habría aliviado un poco las cosas, si no fuera una violación de la regla de variables no finales ). "Esto no es solo una optimización, sino que es crítico cuando consideras que algunas secuencias (como leer las líneas en un archivo) pueden tener efectos secundarios, o puedes tener una secuencia infinita".

  • Podría ejecutarse en paralelo , lo cual es una cosa horrible, horrible para todos menos el 0.1% de su código que necesita ser optimizado. Cualquier código paralelo debe ser pensado (incluso si no usa 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 simples, 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 compilador JIT moderno realiza al ejecutar código.

  • Si necesita paralelismo, probablemente sea mucho más rápido y no mucho más difícil usar un ExecutorService . Los flujos son automáticos (léase: no sé mucho acerca de 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 cosas como el paso a paso pueden no funcionar 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 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 a "en algún lugar donde confundió los tipos, pero no sabemos dónde ni cómo". Del mismo modo, no puede avanzar y examinar las 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 instrucción for-each. ¿Por qué reemplazarlo con una llamada a función? ¿Por qué animar a ocultar los efectos secundarios en algún lugar de las expresiones? ¿Por qué alentar frases poco manejables? Mezclar regularmente para cada uno y nuevo para cada uno es un mal estilo. El código debe hablar en expresiones idiomáticas (patrones que son rápidos de comprender debido a su repetición), y cuanto menos expresiones idiomáticas se usen, más claro será el código y menos tiempo se decidirá qué idioma 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 un método iterator) y no se puede usar en for-each, solo con forEach (). Recomiendo transmitir Streams a Iterables con (Iterable<T>)stream::iterator. Una mejor alternativa es usar StreamEx, que corrige varios problemas de Stream API, incluida la implementación 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 get o set, pero no era segura para subprocesos al iterar.

  • Ejecución paralela (usando una secuencia paralela apropiada) . Esto le ahorra algunas líneas de código en comparación con el uso de 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 presentar más ejemplos)

  • Llamar a una sola función de manera más limpia mediante el uso de forEach()un argumento de referencia de método (es decir, list.forEach (obj::someMethod)). Sin embargo, tenga en cuenta los puntos sobre las excepciones marcadas, 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 lambdas (como http://www.javac.info/closures-v06a.html ) resolvieron algunos de los problemas que mencioné (al agregar sus propias complicaciones, por supuesto).

Aleksandr Dubinsky
fuente
95
"¿Por qué alentar a ocultar los efectos secundarios en algún lugar de las expresiones?" Es la pregunta equivocada. Lo funcional forEachestá ahí para fomentar el estilo funcional, es decir, usar expresiones sin efectos secundarios. Si se encuentra con la situación forEach, no funciona bien con sus efectos secundarios, debe tener la sensación de que no está utilizando la herramienta adecuada para el trabajo. Entonces la respuesta simple es, porque su sentimiento es correcto, así que manténgase en el ciclo for-each para eso. El forbucle clásico no quedó en desuso ...
Holger
17
@Holger ¿Cómo se forEachpuede usar sin efectos secundarios?
Aleksandr Dubinsky
14
De acuerdo, no fui lo suficientemente preciso, forEaches la única operación de transmisión destinada a efectos secundarios, pero no es para efectos secundarios como su código de ejemplo, contar es una reduceoperación típica . Sugeriría, por regla general, mantener todas las operaciones que manipulan variables locales o influirán en el flujo de control (incluido el manejo de excepciones) en un forbucle clásico . Con respecto a la pregunta original, creo, el problema se deriva del hecho de que alguien usa una secuencia donde un simple forbucle sobre la fuente de la secuencia sería suficiente. Use una transmisión donde forEach()solo funciona
Holger
8
@Holger ¿Cuál es un ejemplo de efectos secundarios que forEachsería apropiado?
Aleksandr Dubinsky
29
Algo que procesa cada elemento individualmente y no intenta mutar variables locales. Por ejemplo, manipular los elementos en sí o imprimirlos, escribirlos / enviarlos a un archivo, transmisión de red, etc. No hay problema para mí si cuestiona estos ejemplos y no ve ninguna aplicación para ellos; filtrado, mapeo, reducción, búsqueda y (en menor grado) recolección son las operaciones preferidas de una secuencia. ForEach me parece una conveniencia para vincularme con las API existentes. Y para operaciones paralelas, por supuesto. Estos no funcionarán con forbucles.
Holger
169

La ventaja se tiene en cuenta cuando las operaciones se pueden ejecutar en paralelo. (Ver 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 puede definirse sin tener que decidir si se ejecutará en paralelo o secuencialmente.

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

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

    Tendrá que escribir un código extra para el manejo de hilos, etc.

Nota: Para mi respuesta, supuse que se une a la implementación de la java.util.Streaminterfaz. Si las uniones implementan solo la java.util.Iterableinterfaz, esto ya no es cierto.

mschenk74
fuente
44
Las diapositivas de un ingeniero de oráculo al que hace referencia ( blogs.oracle.com/darcy/resource/Devoxx/… ) no mencionan el paralelismo dentro de esas expresiones lambda. El paralelismo puede ocurrir dentro de los métodos de recolección masiva como map& foldque no están realmente relacionados con lambdas.
Thomas Jungblut
1
Realmente no parece que el código de OP se beneficie del paralelismo automático aquí (especialmente porque no hay garantía de que haya uno). Realmente no sabemos qué es "mIrc", pero "unirse" en realidad no parece algo que pueda extenderse fuera de orden.
Eugene Loy
11
Stream#forEachy Iterable#forEachno son lo mismo OP está preguntando sobre Iterable#forEach.
gvlasov
2
Utilicé el estilo UPDATEX ya que hubo cambios en la especificación entre el momento en que se hizo la pregunta y el momento en que se actualizó la respuesta. Sin la historia de la respuesta, sería aún más confuso, pensé.
mschenk74
1
¿Podría alguien explicarme por qué esta respuesta no es válida si se joinsestá implementando en Iterablelugar de Stream? De un par de cosas que he leído, OP debería poder hacer joins.stream().forEach((join) -> mIrc.join(mSession, join));y joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));si se joinsimplementaIterable
Blueriver
112

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

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

se no pretende ser un atajo para la escritura

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

y ciertamente no debe usarse de esta manera. En su lugar se pretende como un acceso directo (aunque es no exactamente de la misma) para la escritura

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 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 puede reemplazarse 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 un característica que distingue la semántica de Iterable#forEachla semántica de un ciclo 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 inmediatamente obtiene esta información adicional sobre el código.

Los bucles tradicionales para cada uno seguirán siendo una buena práctica (para evitar el término usado en exceso " mejor práctica ") 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 Iterable#forEach, donde tenga sentido.

Desde 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 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 mantenible: el uso de una función como 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 al 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 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 saturar su código principal 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 afirmaciones en este hilo,Iterable#forEach no ya proporcionar un mejor rendimiento que un tradicional para-cada bucle, al menos cuando se utiliza ArrayList y funcionando en el modo hotspot "-client". Si bien este aumento de rendimiento es pequeño y despreciable 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 deben reemplazarse 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#forEachsupera el bucle for tradicional sobre una ArrayList, pero sigue siendo más lento que iterar directamente 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 Iterable#forEachen paralelo usando flujos es sin duda un aspecto importante. Como Collection#parallelStream()no garantiza que el ciclo se ejecute realmente en paralelo, uno debe considerar esto como una característica opcional . Al iterar sobre su lista con list.parallelStream().forEach(...);, usted dice explícitamente: Este ciclo admite la ejecución paralela, pero no depende de ello. De nuevo, 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 algo bueno. Además, si la implementación predeterminada de flujo paralelo no se ajusta a sus necesidades, nadie le impedirá proporcionar su propia implementación. Por ejemplo, puede 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 su implementación de bucle no necesita conocer ni preocuparse por estos detalles.

Balder
fuente
55
Tiene una visión interesante en esta discusión y saca varios puntos. Intentaré dirigirme a ellos. Propone cambiar forEachy for-eachbasarse en algunos criterios con respecto a la naturaleza del cuerpo del bucle. La sabiduría y la disciplina para seguir tales reglas son el sello distintivo de un buen programador. Tales reglas también son su ruina, porque las personas que lo rodean no las siguen o no están de acuerdo. Por ejemplo, usando Excepciones marcadas vs no marcadas. Esta situación parece aún más matizada. Pero, si el cuerpo "no afecta el código de sonido envolvente o el control de flujo", ¿no lo está factorizando mejor como una función?
Aleksandr Dubinsky
44
Gracias por los comentarios detallados Aleksandr. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?. Sí, este será a menudo el caso en mi opinión: factorizar estos bucles como funciones es una consecuencia natural.
Balder
2
Con respecto al problema de rendimiento, supongo que depende mucho de la naturaleza del bucle. En un proyecto en el que estoy trabajando, he estado usando bucles de estilo de función similares a los de Iterable#forEachJava 8 solo por el aumento del rendimiento. El proyecto en cuestión tiene un bucle principal similar a un bucle de juego, con un número indefinido de sub-bucles anidados, donde los clientes pueden conectar a los participantes del bucle como funciones. Tal estructura de software se beneficia enormemente Iteable#forEach.
Balder
77
Hay una oración al final de mi crítica: "El código debe hablar en idiomas, y cuanto menos idiomas se usen, más claro será el código y menos tiempo se decidirá qué idioma usar". Comencé a apreciar profundamente este punto cuando cambié de C # a Java.
Aleksandr Dubinsky
77
Ese es un mal argumento. Podrías usarlo para justificar lo que quieras: por qué no deberías usar un bucle for, porque un bucle while es lo suficientemente bueno y ese es un idioma menos. Diablos, ¿por qué usar cualquier instrucción de bucle, cambio o prueba / captura cuando goto puede hacer todo eso y más?
tapichu
13

forEach()se puede implementar para que sea más rápido que para cada ciclo, porque el iterable conoce la mejor manera de iterar sus elementos, a diferencia del iterador estándar. Entonces, la diferencia es un bucle interno o un 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 ciclo for-each que requiere muchos andamios

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

Sin embargo, también tenemos que contabilizar dos costos generales utilizando 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.

ZhongYu
fuente
8
¿Por qué el iterable conoce la mejor manera pero el iterador no?
mschenk74
2
no hay una diferencia esencial, pero se necesitan códigos adicionales para ajustarse a la interfaz del iterador, lo que puede ser más costoso.
ZhongYu
1
@ zhong.j.yu si implementa la Colección, también implementará Iterable de todos modos. Por lo tanto, no hay sobrecarga de código en términos de "agregar más código para implementar los métodos de interfaz que faltan", si ese es su punto. Como dijo mschenk74, parece que no hay razones por las que no pueda ajustar su iterador para saber cómo iterar sobre su colección de la mejor manera posible. Estoy de acuerdo en que puede haber gastos generales para la creación de iteradores, pero en serio, esas cosas suelen ser tan baratas, que se puede decir que tienen un costo cero ...
Eugene Loy
44
por ejemplo, iterar un árbol: void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}esto es más elegante que la iteración externa, y usted puede decidir cómo sincronizar mejor
monstruo de trinquete
1
Curiosamente, el único comentario en los String.joinmétodos (bueno, unión incorrecta) es "Número de elementos que probablemente no valen Arrays.stream." entonces usan un elegante for loop.
Tom Hawtin - tackline
7

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<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á a todos sus miembros, cada vez a través de un método diferente. en la clase Main ejecuté el método probado tres veces para calentar la JVM. Luego ejecuto el método de prueba 1000 veces sumando el tiempo que toma cada método de iteración (usando System.nanoTime()). Una vez hecho esto, divido esa suma entre 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 de 4 núcleos, 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

Assaf
fuente
23
¿Cómo se obtienen esos números? ¿Qué marco de referencia está utilizando? Si no usa ninguno y simplemente System.out.printlnmuestra estos datos ingenuamente, entonces todos los resultados son inútiles.
Luiggi Mendoza
2
Sin marco Yo uso System.nanoTime(). Si lees la respuesta, verás cómo se hizo. No creo que eso lo haga inútil ya que esta es una pregunta relativa . No me importa qué tan bien funcionó cierto método, me importa qué tan bien lo hizo en comparación con los otros métodos.
Assaf
31
Y ese es el propósito de un buen micro punto de referencia. Como no ha cumplido con estos requisitos, los resultados son inútiles.
Luiggi Mendoza
66
En cambio, puedo recomendar conocer JMH, esto es lo que se está utilizando para Java y pone mucho esfuerzo para obtener los números correctos: openjdk.java.net/projects/code-tools/jmh
dsvensson
1
Estoy de acuerdo con @LuiggiMendoza. No hay forma de saber que estos resultados son consistentes o válidos. Dios sabe cuántos puntos de referencia he hecho que siguen informando resultados diferentes, especialmente según el orden de iteración, el tamaño y lo que no.
mmm
6

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 las ventajas y desventajas 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 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 más que introducir efectos secundarios).

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

Sobre el paralelismo.

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

Según los documentos oficiales de Iterable.forEach :

Realiza la acción dada sobre el contenido del 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, los documentos dejan bastante 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 ellas es necesario que sea más explícito y tenga especial cuidado 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 realizará en paralelo (depende de la colección subyacente).

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

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

En segundo lugar, y más importante: probablemente quiera usar Iterable.forEach solo con lambdas de una línea. Si el "cuerpo" se vuelve más pesado, tienden a no ser tan legibles. Tiene 2 opciones desde aquí: use clases internas (yuck) o use forloop antiguo. La gente a menudo se molesta cuando ve que las mismas cosas (iteraciones sobre colecciones) se realizan en varios estilos / estilos 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.

Eugene Loy
fuente
1
El paralelismo no necesita nuevas "colecciones paralelas". Solo depende de si solicitó una secuencia secuencial (usando collection.stream ()) o una paralela (usando collection.parallelStream ()).
JB Nizet
@JBNizet Según los documentos Collection.parallelStream () no garantiza que la implementación de la colección devolverá un flujo paralelo. De hecho, me pregunto cuándo podría suceder esto, pero probablemente dependa de la colección.
Eugene Loy
convenido. También depende de la colección. Pero mi punto era que los bucles foreach paralelos ya estaban disponibles con todas las colecciones estándar (ArrayList, etc.). No es necesario esperar "colecciones paralelas".
JB Nizet
@JBNizet está de acuerdo con su punto, pero eso no es realmente lo que quise decir con "colecciones paralelas" en primer lugar. Me refiero a Collection.parallelStream () que se agregó en Java 8 como "colecciones paralelas" por analogía con el concepto de Scala que hace más o menos lo mismo. Además, no estoy seguro de cómo se llama en el bit de JSR. Vi un par de artículos que usan la misma terminología para esta característica de Java 8.
Eugene Loy
1
para el último párrafo puede usar una referencia de función:collection.forEach(MyClass::loopBody);
ratchet freak
6

Una de las limitaciones de las funciones más novedosas forEaches la falta de compatibilidad con excepciones comprobadas.

Una posible solución alternativa es reemplazar la terminal forEachcon un antiguo bucle foreach 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í hay una lista de las preguntas más populares con otras soluciones sobre el manejo de excepciones comprobadas en lambdas y streams:

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

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

¿Cómo puedo lanzar excepciones COMPROBADAS desde las secuencias Java 8?

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

Vadzim
fuente
3

La ventaja de Java 1.8 para cada método sobre 1.7 Enhanced for loop es que al escribir código puede centrarse solo en la lógica de negocios.

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

Mira el fragmento de abajo,

  • Aquí he creado una nueva Clase que anulará el método de aceptación de la Clase del Consumidor, donde puede agregar 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);
                }
            });
        }
    }
Hardik Patel
fuente