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

9 minutos de lectura

avatar de usuario
emilien bandolero

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 cuál es 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()); 

  • El segundo. Una función adecuada no debería tener efectos secundarios, en su primera implementación está modificando el mundo externo.

    – Gracias por todo el pescado

    4 de febrero de 2015 a las 10:32

  • solo una cuestión de estilo, pero elt -> elt != null se puede reemplazar con Objects::nonNull

    – the8472

    04/02/2015 a las 10:40

  • @ the8472 Aún mejor sería asegurarse de que no haya valores nulos en la colección en primer lugar, y usar Optional<T> en cambio en combinación con flatMap.

    – Germán

    4 de febrero de 2015 a las 10:47

  • @SzymonRoziewski, no del todo. Para algo tan trivial como esto, el trabajo necesario para configurar el flujo paralelo debajo del capó hará que el uso de esta construcción sea mudo.

    – MK

    4 de febrero de 2015 a las 10:48

  • Tenga en cuenta que puede escribir .map(this::doSomething) asumiendo que doSomething es un método no estático. Si es estático se puede reemplazar this con el nombre de la clase.

    – Germán

    4 de febrero de 2015 a las 11:55

avatar de usuario
Germán

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 canalización de recopilación se escriben secuencialmente: primero una operación de filtro, luego una operación de mapa y luego la recopilación del resultado (para obtener más información sobre los beneficios de las canalizaciones de recopilación, consulte Martin Fowler’s excelente articulo.)

  3. puede cambiar fácilmente la forma en que se recopilan los valores reemplazando el Collector que se usa En algunos casos, es posible que deba escribir su propia Collectorpero el beneficio es que puedes reutilizarlo fácilmente.

avatar de usuario
asilias

Estoy de acuerdo con las respuestas existentes en que la segunda forma es mejor porque no tiene efectos secundarios y es más fácil de paralelizar (solo use una transmisión paralela).

En cuanto al rendimiento, parece que son equivalentes hasta que comienzas a usar secuencias paralelas. En ese caso, mapa funcionará mucho mejor. Vea a continuación el punto de referencia micro resultados:

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 para cada es un método de terminal, devuelve vacío, por lo que está 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()); 

  • Sobre el rendimiento, en su caso, “mapa” realmente gana sobre “forEach” si usa paraleloStreams. Mis benchmarks 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

    23/01/2016 a las 18:00


  • @GiuseppeBertone, depende de assylias, 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 Valéev

    24 de enero de 2016 a las 2:28


avatar de usuario
Craig P. Motlin

Si utiliza Colecciones de Eclipse puedes 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 con entusiasmo y debería ser un poco más rápido que usar Stream.

Nota: Soy un committer de Eclipse Collections.

avatar de usuario
MK

Uno de los principales beneficios del uso de flujos es que brinda la capacidad de procesar datos de forma declarativa, es decir, utilizando un estilo de programación funcional. También brinda capacidad de subprocesos múltiples de forma gratuita, lo que significa que no es necesario escribir ningún código de subprocesos múltiples adicional para que su transmisión sea simultánea.

Suponiendo que la razón por la que está explorando este estilo de programación es que desea explotar estos beneficios, entonces su primera muestra de código no es potencialmente funcional ya que el foreach El 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

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

Otro beneficio con el segundo enfoque es que si la corriente es paralela y el colector es concurrente y desordenado, estas características pueden proporcionar sugerencias útiles para la operación de reducción para realizar la recolección simultáneamente.

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 al forEach.

cuando usas toListla API de secuencias conservará el orden incluso si utiliza una secuencia paralela.

  • No estoy seguro de que este sea un consejo correcto: le vendría bien forEachOrdered en vez de forEach si quisiera usar un flujo paralelo pero aún así preservar el orden. Pero como la documentación para forEach estados, preservar el orden del encuentro sacrifica el beneficio del paralelismo. Sospecho que también es el caso con toList después.

    – Germán

    4 de febrero de 2015 a las 11:08

  • ESTÁ BIEN. De acuerdo a docs.oracle.com/javase/tutorial/collections/streams/… the collect method is designed to perform the most common stream operations that have side effects in a parallel-safe manner. Operations like forEach and peek ... if you use one of these operations with a parallel stream, then the Java runtime may invoke the lambda expression that you specified as its parameter concurrently from multiple threads. Así que cuidadosamente tiene algunas cosas atómicas y otras no. Extraño. Suponiendo que ese sea el caso, la opción 1, si se hace en paralelo, podría tener problemas de contención…

    – rogerdpack

    29 de enero de 2021 a las 17:38


avatar de usuario
Comunidad

Hay una tercera opción: usar stream().toArray() – vea los comentarios debajo de por qué la transmisión no tiene un método toList. Resulta ser más lento que forEach() o collect(), y menos expresivo. Puede optimizarse en versiones posteriores de JDK, así que agréguelo 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, 1 millón de entradas, 20% nulos 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 filtro (por lo que la secuencia es SIZED): toArrays tiene el mejor rendimiento en tal caso, y .forEach() falla con “indexOutOfBounds” en el ArrayList receptor, 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}

  • No estoy seguro de que este sea un consejo correcto: le vendría bien forEachOrdered en vez de forEach si quisiera usar un flujo paralelo pero aún así preservar el orden. Pero como la documentación para forEach estados, preservar el orden del encuentro sacrifica el beneficio del paralelismo. Sospecho que también es el caso con toList después.

    – Germán

    4 de febrero de 2015 a las 11:08

  • ESTÁ BIEN. De acuerdo a docs.oracle.com/javase/tutorial/collections/streams/… the collect method is designed to perform the most common stream operations that have side effects in a parallel-safe manner. Operations like forEach and peek ... if you use one of these operations with a parallel stream, then the Java runtime may invoke the lambda expression that you specified as its parameter concurrently from multiple threads. Así que cuidadosamente tiene algunas cosas atómicas y otras no. Extraño. Suponiendo que ese sea el caso, la opción 1, si se hace en paralelo, podría tener problemas de contención…

    – rogerdpack

    29 de enero de 2021 a las 17:38


avatar de usuario
Juan McClean

Si usar bibliotecas de terceros 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 -> hacerAlgo(elt));

myFinalList no se evalúa hasta el primer acceso (y allí después de que la lista materializada se almacene en caché y se reutilice).

[Disclosure I am the lead developer of cyclops-react]

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad