parallelism java java-8 java-stream spliterator

java - parallelism - Comprender las características del spliterador profundo



java parallelism (1)

Tengo que admitir que también tuve dificultades cuando intenté por primera vez averiguar el significado real de las características y tuve la sensación de que su significado no se resolvió claramente durante la fase de implementación de Java 8 y se usa de manera inconsistente por esa razón.

Considere Spliterator.IMMUTABLE . Spliterator.IMMUTABLE :

Valor característico que significa que la fuente del elemento no puede modificarse estructuralmente; es decir, los elementos no se pueden agregar, reemplazar o eliminar, por lo que dichos cambios no pueden ocurrir durante el recorrido.

Es extraño ver "reemplazado" en esta lista, que generalmente no se considera una modificación estructural cuando se habla de una List o una matriz y, en consecuencia, las fábricas de flujo y spliterator que aceptan una matriz (que no está clonada) informan IMMUTABLE , como LongStream.of(…) o Arrays.spliterator(long[]) .

Si interpretamos esto de manera más generosa como "siempre que el cliente no pueda observarlo", no existe una diferencia significativa con CONCURRENT , ya que en cualquier caso algunos elementos se informarán al cliente sin ninguna forma de reconocer si se agregaron durante el recorrido o si algunos no se informaron debido a la eliminación, ya que no hay forma de rebobinar un spliterator y comparar.

La especificación continúa:

Se espera que un Spliterator que no informa IMMUTABLE o CONCURRENT tenga una política documentada (por ejemplo, lanzar ConcurrentModificationException ) con respecto a la interferencia estructural detectada durante el recorrido.

Y eso es lo único relevante, un spliterator que informa, ya sea IMMUTABLE o CONCURRENT , tiene la garantía de nunca lanzar una ConcurrentModificationException . Por supuesto, CONCURRENT excluye SIZED semántico, pero eso no tiene ninguna consecuencia para el código del cliente.

De hecho, estas características no se usan para nada en la API de Stream, por lo tanto, usarlas de manera inconsistente nunca se notaría en ningún lado.

Esta es también la explicación de por qué cada operación intermedia tiene el efecto de borrar las características CONCURRENT , IMMUTABLE y NONNULL : la implementación de Stream no las usa y sus clases internas que representan el estado de stream no las mantienen.

Del mismo modo, NONNULL no se usa en ninguna parte, por lo que su ausencia para ciertas transmisiones no tiene ningún efecto. Podría rastrear el problema LongStream.of(…) hasta el uso interno de Arrays.spliterator(long[], int, int) que delega a
Spliterators.spliterator​(long[] array, int fromIndex, int toIndex, int additionalCharacteristics) :

El spliterator devuelto siempre informa las características SUBSIZED y SUBSIZED . La persona que llama puede proporcionar características adicionales para que el divisor informe. (Por ejemplo, si se sabe que la matriz no se modificará más, especifique IMMUTABLE ; si se considera que los datos de la matriz tienen un orden de encuentro, especifique ORDERED ). El método Arrays.spliterator(long[], int, int) menudo se puede usar en su lugar, lo que devuelve un spliterator que informa SUBSIZED , SUBSIZED , IMMUTABLE y ORDERED .

Tenga en cuenta (nuevamente) el uso inconsistente de la característica IMMUTABLE . Se trata nuevamente como si tuviera que garantizar la ausencia de cualquier modificación, mientras que al mismo tiempo, Arrays.spliterator y, a su vez, Arrays.stream y LongStream.of(…) informarán la característica IMMUTABLE , incluso por especificación, sin poder Garantía de que la persona que llama no modificará su matriz. A menos que consideremos establecer un elemento que no sea una modificación estructural, pero entonces, la distinción completa se vuelve absurda nuevamente, ya que las matrices no pueden modificarse estructuralmente.

Y claramente no especificó ninguna característica NONNULL . Si bien es obvio que los valores primitivos no pueden ser null , y el Spliterator.Abstract<Primitive>Spliterator inyecta invariablemente una característica NONNULL , el spliterator devuelto por Spliterators.spliterator​(long[],int,int,int) no heredar de Spliterator.AbstractLongSpliterator .

Lo malo es que esto no se puede solucionar sin cambiar la especificación, lo bueno es que no tiene consecuencias de todos modos.

Entonces, si ignoramos cualquier problema con CONCURRENT , IMMUTABLE o NONNULL , que no tiene consecuencias, tenemos

SIZED y skip y limit . Este es un problema bien conocido, resultado de la forma en que Stream API ha implementado la skip y el limit . Otras implementaciones son imaginables. Esto también se aplica a la combinación de un flujo infinito con un limit , que debe tener un tamaño predecible, pero dada la implementación actual, no lo tiene.

Combinando Stream.concat(…) con Stream.empty() . Parece razonable que una secuencia vacía no imponga restricciones en el orden del resultado. Pero el Stream.concat(…) de liberar el orden cuando solo una entrada no tiene orden, es cuestionable. Tenga en cuenta que ser demasiado agresivo con respecto al pedido no es nada nuevo, vea estas preguntas y respuestas sobre un comportamiento que se consideró intencional primero, pero luego se ha solucionado tan tarde como Java 8, actualización 60. Tal vez, Stream.concat debería haberse discutido en este momento de tiempo también ...

El comportamiento de .boxed() es fácil de explicar. Cuando se ha implementado ingenuamente como .mapToObj(Long::valueOf) , simplemente perderá todo el conocimiento, ya que mapToObj no puede asumir que el resultado todavía está ordenado o es distinto. Pero esto se ha solucionado con Java 9. Allí, LongStream.range(0,10).boxed() tiene SUBSIZED|SIZED|ORDERED|SORTED|DISTINCT características, manteniendo todas las características que son relevantes para la implementación.

Con el fin de tratar de comprender profundamente las transmisiones Java y los spliteradores, tengo algunas preguntas sutiles sobre las características del spliterator :

Q1: Stream.empty() vs Stream.of() (Stream.of () sin argumentos)

  • Stream.empty() : SUBSIZADO, TAMAÑO
  • Stream.of() : SUBSIZADO, INMUTABLE , TAMAÑO, ORDENADO

¿Por qué Stream.empty() no tiene las mismas características de Stream.of() ? Tenga en cuenta que tiene impactos cuando se usa junto con Stream.concat () (especialmente sin haber ORDERED ). Yo diría que Stream.empty() no solo debe haber sido INMUTABLE y ORDENADO, sino también DISTINCT y NONNULL . También tiene sentido Stream.of() con solo un argumento que tiene DISTICT .

Q2: LongStream.of() no tiene NONNULL

Acabo de notar que NONNULL no está disponible en LongStream.of . ¿No es NONNULL una característica principal de todos los LongStream s, IntStream s y DoubleStream s?

Q3: LongStream.range(,) vs LongStream.range(,).boxed()

  • LongRange.range(,) : SUBSIZADO, INMUTABLE, NO NULL , TAMAÑO , ORDENADO , CLASIFICADO , DISTINTO
  • LongStream.range(,).boxed() Boxed LongStream.range(,).boxed() : SUBSIZED, SIZED, ORDERED

¿Por qué .boxed() pierde todas estas características? No debería perder ninguno.

Entiendo que .mapToObj() puede perder NONNULL, INMUTABLE y DISTICT , pero .boxed() ... no tiene sentido.

P4: .peek() pierde INMUTABLE y NO NULL

LongStream.of(1) : SUBSIZADO, INMUTABLE, NO NULL, TAMAÑO, ... LongStream.of(1).peek() : SUBSIZADO, TAMAÑO, ...

¿Por qué .peek() pierde estas características? .peek realmente no debería perder ninguno.

Q5: .skip() , .limit() pierde SUBSIZED, INMUTABLE, NONULL, SIZED

Solo observe que estas operaciones pierden SUBSIZADO, INMUTABLE, NO NULL, TAMAÑO . ¿Por qué? Si el tamaño está disponible, entonces también es fácil calcular el tamaño final.

Q6: .filter() pierde INMUTABLE, NO NULL

Solo tenga en cuenta que esta operación también pierde SUBSIZADO, INMUTABLE, NO NULL, TAMAÑO . Tiene sentido perder SUBSIZADO y TAMAÑO , pero los otros dos no tienen sentido. ¿Por qué?

Apreciaré si alguien que entiende profundamente el separador podría aportar algo de claridad. Gracias.