Tengo una lista myListToParse
donde quiero filtrar los elementos y aplicar un método en cada elemento, y agregar el resultado en otra lista myFinalList
.
Con Java 8 noté que puedo hacerlo de 2 maneras diferentes. Me gustaría saber la forma más eficiente entre ellos y entender por qué una forma es mejor que la otra.
Estoy abierto a cualquier sugerencia sobre una tercera vía.
Método 1:
myFinalList = new ArrayList<>();
myListToParse.stream()
.filter(elt -> elt != null)
.forEach(elt -> myFinalList.add(doSomething(elt)));
Método 2:
myFinalList = myListToParse.stream()
.filter(elt -> elt != null)
.map(elt -> doSomething(elt))
.collect(Collectors.toList());
java
java-8
java-stream
Emilien Brigand
fuente
fuente
elt -> elt != null
puede ser reemplazado conObjects::nonNull
Optional<T>
en combinación conflatMap
..map(this::doSomething)
suponiendo quedoSomething
es un método no estático. Si es estático, puede reemplazarlothis
con el nombre de la clase.Respuestas:
No se preocupe por las diferencias de rendimiento, normalmente serán mínimas en este caso.
El método 2 es preferible porque
no requiere mutar una colección que existe fuera de la expresión lambda,
es más legible porque los diferentes pasos que se realizan en la tubería de recolección se escriben secuencialmente: primero una operación de filtro, luego una operación de mapa, luego recolectando el resultado (para obtener más información sobre los beneficios de las tuberías de recolección, consulte el excelente artículo de Martin Fowler ),
puede cambiar fácilmente la forma en que se recopilan los valores reemplazando el
Collector
que se utiliza. En algunos casos, es posible que deba escribir el suyoCollector
, pero el beneficio es que puede reutilizarlo fácilmente.fuente
Estoy de acuerdo con las respuestas existentes de que la segunda forma es mejor porque no tiene ningún efecto secundario y es más fácil de paralelizar (solo use una secuencia paralela).
En cuanto al rendimiento, parece que son equivalentes hasta que comience a utilizar flujos paralelos. En ese caso, el mapa funcionará mucho mejor. Vea a continuación los resultados del micro benchmark :
No puede impulsar el primer ejemplo de la misma manera porque forEach es un método terminal, devuelve nulo, por lo que se ve obligado a usar una lambda con estado. Pero eso es realmente una mala idea si está utilizando flujos paralelos .
Finalmente, tenga en cuenta que su segundo fragmento se puede escribir de una manera un poco más concisa con referencias de métodos e importaciones estáticas:
fuente
Uno de los principales beneficios del uso de flujos es que brinda la capacidad de procesar datos de manera declarativa, es decir, usando un estilo funcional de programación. También ofrece la capacidad de subprocesos múltiples de forma gratuita, lo que significa que no es necesario escribir ningún código adicional de subprocesos múltiples para que su transmisión sea concurrente.
Suponiendo que la razón por la que está explorando este estilo de programación es que desea aprovechar estos beneficios, entonces su primer ejemplo de código posiblemente no sea funcional, ya que el
foreach
método se clasifica como terminal (lo que significa que puede producir efectos secundarios).Se prefiere la segunda forma desde el punto de vista de la programación funcional, ya que la función de mapa puede aceptar funciones lambda sin estado. Más explícitamente, la lambda pasada a la función de mapa debería ser
ArrayList
).Otro beneficio con el segundo enfoque es que si la secuencia es paralela y el recolector es concurrente y desordenado, estas características pueden proporcionar sugerencias útiles para la operación de reducción para realizar la recopilación al mismo tiempo.
fuente
Si usa Eclipse Collections , puede usar el
collectIf()
método.Se evalúa ansiosamente y debería ser un poco más rápido que usar un Stream.
Nota: Soy un committer para Eclipse Collections.
fuente
Prefiero la segunda forma.
Cuando usa la primera forma, si decide usar una secuencia paralela para mejorar el rendimiento, no tendrá control sobre el orden en que los elementos se agregarán a la lista de salida
forEach
.Cuando lo use
toList
, la API de Streams preservará el orden incluso si usa un flujo paralelo.fuente
forEachOrdered
lugar deforEach
si quisiera usar una secuencia paralela pero aún así preservar el orden. Pero como la documentación para losforEach
estados, preservar el orden del encuentro sacrifica el beneficio del paralelismo. Sospecho que ese es también el caso contoList
entonces.Hay una tercera opción: usar
stream().toArray()
: ver los comentarios en por qué la transmisión no tiene un método toList . Resulta ser más lento que forEach () o collect (), y menos expresivo. Es posible que se optimice en versiones posteriores de JDK, por lo que debe agregarlo aquí por si acaso.asumiendo
List<String>
con un punto de referencia micro-micro, entradas de 1M, nulos del 20% y transformación simple en doSomething ()
los resultados son
paralela:
secuencial:
paralelo sin nulos y sin filtro (por lo que el flujo es
SIZED
): toArrays tiene el mejor rendimiento en tal caso, y.forEach()
falla con "indexOutOfBounds" en el receptor ArrayList, tuvo que reemplazar con.forEachOrdered()
fuente
Puede ser el Método 3.
Siempre prefiero mantener la lógica separada.
fuente
Si usar 3rd Pary Libaries está bien, cyclops-react define colecciones extendidas Lazy con esta funcionalidad incorporada. Por ejemplo, podríamos simplemente escribir
ListX myListToParse;
ListX myFinalList = myListToParse.filter (elt -> elt! = Null) .map (elt -> doSomething (elt));
myFinalList no se evalúa hasta el primer acceso (y allí después de que la lista materializada se almacena en caché y se reutiliza).
[Divulgación Soy el desarrollador principal de cyclops-react]
fuente