streams - Java 8 Stream API: ¿Una operación intermedia con estado garantiza una nueva colección de origen?
procesamiento de datos con streams de java se 8-parte 2 (3)
La sorted
correcta debe ser una barrera de copia completa para el flujo de datos, ya que no se pudo clasificar su fuente; pero esto no está documentado como tal, por lo tanto no confíe en ello.
Esto no se trata solo de la sorted
en sí, sino de la optimización que se puede hacer en el flujo de datos, por lo que la sorted
se puede omitir por completo. Por ejemplo:
List<Integer> sortedList = IntStream.range(0, 10)
.boxed()
.collect(Collectors.toList());
StreamSupport.stream(() -> sortedList.spliterator(), Spliterator.SORTED, false)
.sorted()
.forEach(sortedList::remove); // fails with CME, thus no copying occurred
Por supuesto, la sorted
debe ser una barrera completa y detenerse para hacer un tipo completo, a menos que, por supuesto, se pueda omitir, por lo que la documentación no hace tales promesas, por lo que no nos encontramos con sorpresas extrañas.
distinct
por otra parte, no tiene que ser una barrera completa , todo lo que hace es controlar un elemento a la vez, si es único; así que después de que se verifica un único elemento (y es único), se pasa a la siguiente etapa, por lo tanto, sin ser una barrera completa. De cualquier manera, esto no está documentado también ...
¿Es verdadera la siguiente afirmación?
( Source y source : parece que se copian entre sí o provienen de la misma fuente).
La operación
sorted()
es una "operación intermedia con estado", lo que significa que las operaciones subsiguientes ya no funcionan en la recopilación de respaldo, sino en un estado interno.
He probado Stream::sorted
como un fragmento de las fuentes anteriores:
final List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());
list.stream()
.filter(i -> i > 5)
.sorted()
.forEach(list::remove);
System.out.println(list); // Prints [0, 1, 2, 3, 4, 5]
Funciona. Reemplacé Stream::sorted
con Stream::distinct
, Stream::limit
y Stream::skip
:
final List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());
list.stream()
.filter(i -> i > 5)
.distinct()
.forEach(list::remove); // Throws NullPointerException
Para mi sorpresa, se lanza la NullPointerException
.
Todos los métodos probados siguen las características de operación intermedia con estado . Sin embargo, este comportamiento único de Stream::sorted
no está documentado ni la parte de las operaciones y tuberías de Stream explica si las operaciones intermedias con estado realmente garantizan una nueva colección de origen.
¿De dónde viene mi confusión y cuál es la explicación del comportamiento anterior?
La documentación de la API no ofrece tal garantía "de que las operaciones posteriores ya no operen en la colección de respaldo", por lo tanto, nunca debe confiar en el comportamiento de una implementación en particular.
Tu ejemplo pasa por hacer lo deseado por accidente; ni siquiera existe una garantía de que la List
creada por collect(Collectors.toList())
compatible con la operación de remove
.
Para mostrar un contra-ejemplo
Set<Integer> set = IntStream.range(0, 10).boxed()
.collect(Collectors.toCollection(TreeSet::new));
set.stream()
.filter(i -> i > 5)
.sorted()
.forEach(set::remove);
lanza una ConcurrentModificationException
. La razón es que la implementación optimiza este escenario, ya que la fuente ya está ordenada. En principio, podría hacer la misma optimización para su ejemplo original, ya que forEach
está realizando explícitamente la acción en un orden no especificado, por lo tanto, la clasificación no es necesaria.
Hay otras optimizaciones imaginables, por ejemplo, sorted().findFirst()
podría convertirse en una operación de "encontrar el mínimo", sin la necesidad de copiar el elemento en un nuevo almacenamiento para clasificar.
Por lo tanto, la conclusión es que, al depender de un comportamiento no especificado, lo que puede suceder hoy en día puede romperse mañana, cuando se agreguen nuevas optimizaciones.
No debería haber mencionado los casos con una operación de terminal para forEach(list::remove)
porque list::remove
es una función de interferencia y viola el principio de "non-interference" para las acciones de la terminal.
Es vital seguir las reglas antes de preguntarse por qué un fragmento de código incorrecto causa un comportamiento inesperado (o no documentado).
Creo que la list::remove
es la raíz del problema aquí. No habría notado la diferencia entre las operaciones para este escenario si hubiera escrito una acción adecuada para forEach
.