Javadoc of Collector muestra cómo recopilar elementos de una secuencia en una nueva Lista. ¿Hay una línea que agrega los resultados a una ArrayList existente?
Javadoc of Collector muestra cómo recopilar elementos de una secuencia en una nueva Lista. ¿Hay una línea que agrega los resultados a una ArrayList existente?
Respuestas:
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 Collector
para mutar una colección existente.
La respuesta corta es no , al menos, no en general, no debe usar a Collector
para 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
toCollection
mé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.
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()
.
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.
forEach(existing::add)
como posibilidad en una respuesta hace dos meses . Debería haber agregado forEachOrdered
también ...
forEachOrdered
lugar de forEach
?
forEachOrdered
funciona tanto para secuencias secuenciales como paralelas . Por el contrario, forEach
podrí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>
.
target::add
. Independientemente de qué hilos se invoque el método, no hay carrera de datos . Hubiera esperado que lo supieras.
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 list
variable; 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 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));
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.
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.
Collection
"