¿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?
java
for-loop
java-8
java-stream
nebkat
fuente
fuente
joins.forEach((join) -> mIrc.join(mSession, join));
realmente una "simplificación" defor (String join : joins) { mIrc.join(mSession, join); }
? Has aumentado el recuento de puntuación de 9 a 12, en beneficio de ocultar el tipo dejoin
. Lo que realmente has hecho es poner dos declaraciones en una línea.Respuestas:
La mejor práctica es usar
for-each
. Además de violar el principio estúpido Keep It Simple, el nuevoforEach()
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:
No se pueden manejar las excepciones marcadas . Las lambdas en realidad no tienen prohibido lanzar excepciones marcadas, pero las interfaces funcionales comunes como
Consumer
no declaran ninguna. Por lo tanto, cualquier código que arroje excepciones marcadas debe incluirlas entry-catch
oThrowables.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
return
en una lambda es igual a acontinue
en for-each, pero no hay equivalente a abreak
. 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
Stream
no se implementaIterable
(a pesar de tener un métodoiterator
) 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ónIterable
.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).
fuente
forEach
está ahí para fomentar el estilo funcional, es decir, usar expresiones sin efectos secundarios. Si se encuentra con la situaciónforEach
, 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. Elfor
bucle clásico no quedó en desuso ...forEach
puede usar sin efectos secundarios?forEach
es 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 unareduce
operació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 unfor
bucle clásico . Con respecto a la pregunta original, creo, el problema se deriva del hecho de que alguien usa una secuencia donde un simplefor
bucle sobre la fuente de la secuencia sería suficiente. Use una transmisión dondeforEach()
solo funcionaforEach
sería apropiado?for
bucles.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
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.Stream
interfaz. Si las uniones implementan solo lajava.util.Iterable
interfaz, esto ya no es cierto.fuente
map
&fold
que no están realmente relacionados con lambdas.Stream#forEach
yIterable#forEach
no son lo mismo OP está preguntando sobreIterable#forEach
.joins
está implementando enIterable
lugar deStream
? De un par de cosas que he leído, OP debería poder hacerjoins.stream().forEach((join) -> mIrc.join(mSession, join));
yjoins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
si sejoins
implementaIterable
Al leer esta pregunta, uno puede tener la impresión de que,
Iterable#forEach
en 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:se no pretende ser un atajo para la escritura
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
Y es como un reemplazo para el siguiente código Java 7:
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#forEach
la semántica de un ciclo tradicional para cada bucle. Una vez que uno se acostumbra a la sintaxis deIterable#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#forEach
deba 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 unoIterable#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 usarIterable#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:
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 reemplazarseIterable#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):
Y aquí están los resultados:
Cuando se ejecuta con "-client",
Iterable#forEach
supera 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#forEach
en paralelo usando flujos es sin duda un aspecto importante. ComoCollection#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 conlist.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:
Lo bueno aquí es que su implementación de bucle no necesita conocer ni preocuparse por estos detalles.
fuente
forEach
yfor-each
basarse 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?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.Iterable#forEach
Java 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 enormementeIteable#forEach
.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 comoa diferencia del ciclo for-each que requiere muchos andamios
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.
fuente
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 mejorString.join
métodos (bueno, unión incorrecta) es "Número de elementos que probablemente no valen Arrays.stream." entonces usan un elegante for loop.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:
for
List.forEach()
List.stream().forEach()
List.parallelStream().forEach
el procedimiento de prueba y los parámetros
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 (usandoSystem.nanoTime()
). Una vez hecho esto, divido esa suma entre 1000 y ese es el resultado, el tiempo promedio. ejemplo:Ejecuté esto en una CPU i5 de 4 núcleos, con la versión java 1.8.0_05
clásico
for
tiempo de ejecución: 4.21 ms
foreach clásico
tiempo de ejecución: 5.95 ms
List.forEach()
tiempo de ejecución: 3.11 ms
List.stream().forEach()
tiempo de ejecución: 2.79 ms
List.parallelStream().forEach
tiempo de ejecución: 3.6 ms
fuente
System.out.println
muestra estos datos ingenuamente, entonces todos los resultados son inútiles.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.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 :
... 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.
fuente
collection.forEach(MyClass::loopBody);
Una de las limitaciones de las funciones más novedosas
forEach
es la falta de compatibilidad con excepciones comprobadas.Una posible solución alternativa es reemplazar la terminal
forEach
con un antiguo bucle foreach simple: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?
fuente
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 ...!
fuente