Java 8: la mejor manera de transformar una lista: ¿mapa o foreach?

188

Tengo una lista myListToParsedonde 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()); 
Emilien Brigand
fuente
55
El segundo. Una función adecuada no debería tener efectos secundarios, en su primera implementación está modificando el mundo externo.
ThanksForAllTheFish
37
sólo una cuestión de estilo, pero elt -> elt != nullpuede ser reemplazado conObjects::nonNull
the8472
2
@ the8472 Incluso mejor sería asegurarse de que no haya valores nulos en la colección en primer lugar, y utilizarlos Optional<T>en combinación con flatMap.
herman
2
@SzymonRoziewski, no del todo. Para algo tan trivial como esto, el trabajo necesario para configurar la corriente paralela debajo del capó hará que el uso de esta construcción sea silencioso.
MK
2
Tenga en cuenta que puede escribir .map(this::doSomething)suponiendo que doSomethinges un método no estático. Si es estático, puede reemplazarlo thiscon el nombre de la clase.
herman

Respuestas:

153

No se preocupe por las diferencias de rendimiento, normalmente serán mínimas en este caso.

El método 2 es preferible porque

  1. no requiere mutar una colección que existe fuera de la expresión lambda,

  2. 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 ),

  3. puede cambiar fácilmente la forma en que se recopilan los valores reemplazando el Collectorque se utiliza. En algunos casos, es posible que deba escribir el suyo Collector, pero el beneficio es que puede reutilizarlo fácilmente.

Germán
fuente
43

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 :

Benchmark                         Mode  Samples    Score   Error  Units
SO28319064.forEach                avgt      100  187.310 ± 1.768  ms/op
SO28319064.map                    avgt      100  189.180 ± 1.692  ms/op
SO28319064.mapWithParallelStream  avgt      100   55,577 ± 0,782  ms/op

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:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 
asilias
fuente
1
Sobre el rendimiento, en su caso, "map" realmente gana sobre "forEach" si usa parallelStreams. Mis benchmaks en milisegundos: SO28319064.forEach: 187,310 ± 1,768 ms / op - SO28319064.map: 189,180 ± 1,692 ms / op --SO28319064.mapParallelStream: 55,577 ± 0,782 ms / op
Giuseppe Bertone el
2
@GiuseppeBertone, depende de las asilias, pero en mi opinión, su edición contradice la intención del autor original. Si desea agregar su propia respuesta, es mejor agregarla en lugar de editar tanto la existente. Además, ahora el enlace al microbenchmark no es relevante para los resultados.
Tagir Valeev
5

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 foreachmé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

  1. No interfiere, lo que significa que la función no debe alterar la fuente de la secuencia si no es concurrente (por ejemplo ArrayList).
  2. Sin estado para evitar resultados inesperados al hacer un procesamiento paralelo (causado por diferencias de programación de subprocesos).

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.

MK
fuente
4

Si usa Eclipse Collections , puede usar el collectIf()método.

MutableList<Integer> source =
    Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);

MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);

Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

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.

Craig P. Motlin
fuente
1

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.

Eran
fuente
No estoy seguro de que este sea el consejo correcto: podría usarlo en forEachOrderedlugar de forEachsi quisiera usar una secuencia paralela pero aún así preservar el orden. Pero como la documentación para los forEachestados, preservar el orden del encuentro sacrifica el beneficio del paralelismo. Sospecho que ese es también el caso con toListentonces.
herman
0

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>

    myFinalList = Arrays.asList(
            myListToParse.stream()
                    .filter(Objects::nonNull)
                    .map(this::doSomething)
                    .toArray(String[]::new)
    );

con un punto de referencia micro-micro, entradas de 1M, nulos del 20% y transformación simple en doSomething ()

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
    long[] timing = new long[samples];
    for (int i = 0; i < samples; i++) {
        long start = System.currentTimeMillis();
        methodToTest.run();
        timing[i] = System.currentTimeMillis() - start;
    }
    final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
    System.out.println(testName + ": " + stats);
    return stats;
}

los resultados son

paralela:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

secuencial:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

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

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}
harshtuna
fuente
0

Puede ser el Método 3.

Siempre prefiero mantener la lógica separada.

Predicate<Long> greaterThan100 = new Predicate<Long>() {
            @Override
            public boolean test(Long currentParameter) {
                return currentParameter > 100;
            }
        };

        List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
        List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());
Kumar Abhishek
fuente
0

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]

John McClean
fuente