Cómo agregar elementos de una secuencia Java8 a una lista existente

Respuestas:

198

NOTA: la respuesta de nosid muestra cómo agregar a una colección existente usando forEachOrdered(). Esta es una técnica útil y efectiva para mutar colecciones existentes. Mi respuesta aborda por qué no debe usar a Collectorpara mutar una colección existente.

La respuesta corta es no , al menos, no en general, no debe usar a Collectorpara modificar una colección existente.

La razón es que los colectores están diseñados para admitir paralelismo, incluso en colecciones que no son seguras para subprocesos. La forma en que lo hacen es hacer que cada hilo opere independientemente en su propia colección de resultados intermedios. La forma en que cada subproceso obtiene su propia colección es llamar a la Collector.supplier()que se requiere para devolver una nueva colección cada vez.

Estas colecciones de resultados intermedios se fusionan, nuevamente de manera confinada, hasta que haya una única colección de resultados. Este es el resultado final de la collect()operación.

Un par de respuestas de Balder y Assylias sugirieron usar Collectors.toCollection()y luego pasar a un proveedor que devuelve una lista existente en lugar de una nueva lista. Esto viola el requisito del proveedor, que es que devuelve una nueva colección vacía cada vez.

Esto funcionará para casos simples, como lo demuestran los ejemplos en sus respuestas. Sin embargo, fallará, particularmente si la secuencia se ejecuta en paralelo. (Una versión futura de la biblioteca puede cambiar de alguna manera imprevista que hará que falle, incluso en el caso secuencial).

Tomemos un ejemplo simple:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

Cuando ejecuto este programa, a menudo obtengo un ArrayIndexOutOfBoundsException. Esto se debe a que varios subprocesos están funcionando ArrayList, una estructura de datos insegura de subprocesos. OK, hagámoslo sincronizado:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

Esto ya no fallará con una excepción. Pero en lugar del resultado esperado:

[foo, 0, 1, 2, 3]

da resultados extraños como este:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

Este es el resultado de las operaciones de acumulación / fusión confinadas en hilos que describí anteriormente. Con una secuencia paralela, cada subproceso llama al proveedor para obtener su propia colección para la acumulación intermedia. Si pasa un proveedor que devuelve la misma colección, cada hilo agrega sus resultados a esa colección. Como no hay orden entre los hilos, los resultados se agregarán en un orden arbitrario.

Luego, cuando se fusionan estas colecciones intermedias, esto básicamente combina la lista consigo misma. Las listas se fusionan usando List.addAll(), lo que dice que los resultados no están definidos si la colección de origen se modifica durante la operación. En este caso, ArrayList.addAll()realiza una operación de copia de matriz, por lo que termina duplicándose, que es algo de lo que uno esperaría, supongo. (Tenga en cuenta que otras implementaciones de List podrían tener un comportamiento completamente diferente). De todos modos, esto explica los resultados extraños y los elementos duplicados en el destino.

Podrías decir: "Solo me aseguraré de ejecutar mi secuencia secuencialmente" y seguir adelante y escribir código como este

stream.collect(Collectors.toCollection(() -> existingList))

de todas formas. Recomiendo no hacer esto. Si controlas la transmisión, seguro, puedes garantizar que no se ejecutará en paralelo. Espero que surja un estilo de programación donde se transmitan las transmisiones en lugar de las colecciones. Si alguien le entrega una transmisión y usted usa este código, fallará si la transmisión es paralela. Peor aún, alguien podría entregarle una secuencia secuencial y este código funcionará bien por un tiempo, pasar todas las pruebas, etc. Luego, una cantidad arbitraria de tiempo más tarde, el código en otra parte del sistema puede cambiar para usar secuencias paralelas que causarán su código romper.

OK, solo asegúrate de recordar llamar sequential()a cualquier transmisión antes de usar este código:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

Por supuesto, recordarás hacer esto siempre, ¿verdad? :-) Digamos que lo haces. Entonces, el equipo de rendimiento se preguntará por qué todas sus implementaciones paralelas cuidadosamente diseñadas no están proporcionando ninguna aceleración. Y una vez más, lo rastrearán hasta su código, lo que está obligando a toda la transmisión a ejecutarse secuencialmente.

No lo hagas

Stuart Marks
fuente
¡Gran explicación! Gracias por aclarar esto. Editaré mi respuesta para recomendar nunca hacer esto con posibles flujos paralelos.
Balder
3
Si la pregunta es, si hay una línea para agregar elementos de una secuencia a una lista existente, entonces la respuesta corta es . Mira mi respuesta. Sin embargo, estoy de acuerdo con usted en que usar Collectors.toCollection () en combinación con una lista existente es la manera incorrecta.
nosid
Cierto. Supongo que el resto de nosotros estábamos pensando en coleccionistas.
Stuart Marks
¡Gran respuesta! Estoy muy tentado a usar la solución secuencial, incluso si usted desaconseja claramente porque, como se ha dicho, debe funcionar bien. Pero el hecho de que el javadoc requiera que el argumento del proveedor del toCollectionmétodo devuelva una colección nueva y vacía cada vez me convence de no hacerlo. Tengo muchas ganas de romper el contrato javadoc de las clases principales de Java.
zoom
1
@AlexCurvers Si desea que la transmisión tenga efectos secundarios, seguramente quiera usarla forEachOrdered. Los efectos secundarios incluyen agregar elementos a una colección existente, independientemente de si ya tiene elementos. Si desea que los elementos de una secuencia se coloquen en una nueva colección, use collect(Collectors.toList())o toSet()o toCollection().
Stuart Marks
169

Hasta donde puedo ver, todas las otras respuestas hasta ahora utilizaron un recopilador para agregar elementos a una secuencia existente. Sin embargo, hay una solución más corta y funciona tanto para secuencias secuenciales como paralelas. Simplemente puede usar el método forEachOrdered en combinación con una referencia de método.

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

La única restricción es que el origen y el destino son listas diferentes, ya que no está permitido realizar cambios en el origen de una secuencia siempre que se procese.

Tenga en cuenta que esta solución funciona tanto para secuencias secuenciales como paralelas. Sin embargo, no se beneficia de la concurrencia. La referencia de método pasada a forEachOrdered siempre se ejecutará secuencialmente.

nosid
fuente
66
+1 Es curioso cómo tantas personas afirman que no hay posibilidad cuando hay una. Por cierto. Incluí forEach(existing::add)como posibilidad en una respuesta hace dos meses . Debería haber agregado forEachOrderedtambién ...
Holger
55
¿Hay alguna razón que usaste en forEachOrderedlugar de forEach?
membersound
66
@membersound: forEachOrderedfunciona tanto para secuencias secuenciales como paralelas . Por el contrario, forEachpodría ejecutar el objeto de función pasado simultáneamente para flujos paralelos. En este caso, el objeto de función debe sincronizarse correctamente, por ejemplo, mediante el uso de a Vector<Integer>.
nosid
@BrianGoetz: Tengo que admitir que la documentación de Stream.forEachOrdered es un poco imprecisa. Sin embargo, no puedo ver ninguna interpretación razonable de esta especificación , en la que no exista una relación antes de que ocurra entre dos llamadas de target::add. Independientemente de qué hilos se invoque el método, no hay carrera de datos . Hubiera esperado que lo supieras.
nosid
Esta es la respuesta más útil, en lo que a mí respecta. En realidad, muestra una forma práctica de insertar elementos en una lista existente de una secuencia, que es lo que la pregunta solicitó (a pesar de la palabra engañosa "recoger")
Wheezil
12

La respuesta corta es no (o debería ser no). EDITAR: sí, es posible (ver la respuesta de Assylias a continuación), pero sigue leyendo. EDIT2: ¡ pero mira la respuesta de Stuart Marks por otra razón más por la que aún no deberías hacerlo!

La respuesta más larga:

El propósito de estas construcciones en Java 8 es introducir algunos conceptos de programación funcional en el lenguaje; En la programación funcional, las estructuras de datos no suelen modificarse, en cambio, se crean nuevas a partir de las antiguas mediante transformaciones como mapa, filtro, plegado / reducción y muchas otras.

Si debe modificar la lista anterior, simplemente recopile los elementos asignados en una lista nueva:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

y luego hazlo list.addAll(newList)otra vez: si realmente debes hacerlo.

(o construya una nueva lista que concatene la anterior y la nueva y asígnela nuevamente a la listvariable; esto es un poco más en el espíritu de FP que addAll)

En cuanto a la API: a pesar de que la API lo permite (de nuevo, vea la respuesta de las asilias), debe tratar de evitar hacerlo independientemente, al menos en general. Es mejor no luchar contra el paradigma (FP) e intentar aprenderlo en lugar de luchar contra él (aunque Java generalmente no es un lenguaje FP), y solo recurrir a tácticas "más sucias" si es absolutamente necesario.

La respuesta realmente larga: (es decir, si incluye el esfuerzo de encontrar y leer una introducción / libro de FP como se sugiere)

Para saber por qué modificar las listas existentes es, en general, una mala idea y conduce a un código menos mantenible, a menos que esté modificando una variable local y su algoritmo sea corto y / o trivial, lo que está fuera del alcance de la cuestión de la mantenibilidad del código : Encuentre una buena introducción a la Programación funcional (hay cientos) y comience a leer. Una explicación de "vista previa" sería algo así como: es matemáticamente más sólido y más fácil razonar sobre no modificar los datos (en la mayoría de las partes de su programa) y conduce a un nivel más alto y menos técnico (así como más amigable para los humanos, una vez que su cerebro transiciones fuera de las definiciones de pensamiento lógico de estilo antiguo) de la lógica del programa

Erik Kaplun
fuente
@assylias: lógicamente, no estuvo mal porque había la parte o ; de todos modos, agregó una nota.
Erik Kaplun
1
La respuesta corta es correcta. Las frases propuestas tendrán éxito en casos simples pero fracasarán en el caso general.
Stuart Marks
La respuesta más larga es mayormente correcta, pero el diseño de la API se trata principalmente de paralelismo y no tanto de programación funcional. Aunque, por supuesto, hay muchas cosas sobre FP que son susceptibles de paralelismo, por lo que estos dos conceptos están bien alineados.
Stuart Marks
@StuartMarks: Interesante: ¿en qué casos se rompe la solución provista en la respuesta de Assylias? (y buenos puntos sobre el paralelismo, supongo que estaba demasiado ansioso por abogar por FP)
Erik Kaplun
@ErikAllik He agregado una respuesta que cubre este problema.
Stuart Marks
11

Erik Allik ya dio muy buenas razones, por las cuales es muy probable que no desee recopilar elementos de una secuencia en una Lista existente.

De todos modos, puede usar el siguiente one-liner, si realmente necesita esta funcionalidad.

Pero como Stuart Marks explica en su respuesta, nunca debe hacer esto, si las transmisiones pueden ser paralelas, use bajo su propio riesgo ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));
Balder
fuente
ahh, eso es una pena: P
Erik Kaplun
2
Esta técnica fallará horriblemente si la secuencia se ejecuta en paralelo.
Stuart Marks
1
Sería responsabilidad del proveedor de la colección asegurarse de que no falla, por ejemplo, proporcionando una colección concurrente.
Balder
2
No, este código viola el requisito de toCollection (), que es que el proveedor devuelve una nueva colección vacía del tipo apropiado. Incluso si el destino es seguro para subprocesos, la fusión que se realiza para el caso paralelo dará lugar a resultados incorrectos.
Stuart Marks
1
@Balder He agregado una respuesta que debería aclarar esto.
Stuart Marks
4

Solo tiene que referir su lista original para que sea la que Collectors.toList()devuelva.

Aquí hay una demostración:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

Y así es como puede agregar los elementos recién creados a su lista original en una sola línea.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

Eso es lo que proporciona este Paradigma de programación funcional.

Aman Agnihotri
fuente
Quise decir cómo agregar / recopilar en una lista existente, no solo reasignar.
codefx
1
Bueno, técnicamente no puedes hacer ese tipo de cosas en el paradigma de Programación Funcional, de lo que se trata la transmisión. En la programación funcional, el estado no se modifica, en su lugar, se crean nuevos estados en estructuras de datos persistentes, lo que lo hace seguro para propósitos de concurrencia y más funcional. El enfoque que mencioné es lo que puede hacer, o puede recurrir al enfoque orientado a objetos de estilo antiguo donde itera sobre cada elemento y mantiene o elimina los elementos como mejor le parezca.
Aman Agnihotri
0

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());

AS Ranjan
fuente
0

Concatenaría la lista anterior y la nueva lista como secuencias y guardaría los resultados en la lista de destinos. Funciona bien en paralelo, también.

Usaré el ejemplo de respuesta aceptada dada por Stuart Marks:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

Espero eso ayude.

Nikos Stais
fuente