¿Por qué Iterable <T> no proporciona los métodos stream () y parallelStream ()?

241

Me pregunto por qué la Iterableinterfaz no proporciona los métodos stream()y parallelStream(). Considere la siguiente clase:

public class Hand implements Iterable<Card> {
    private final List<Card> list = new ArrayList<>();
    private final int capacity;

    //...

    @Override
    public Iterator<Card> iterator() {
        return list.iterator();
    }
}

Es una implementación de una Mano, ya que puedes tener cartas en tu mano mientras juegas un Juego de Cartas Coleccionables.

Esencialmente, envuelve a List<Card>, asegura una capacidad máxima y ofrece algunas otras características útiles. Es mejor implementarlo directamente como a List<Card>.

Ahora, para mayor comodidad, pensé que sería bueno implementarlo Iterable<Card>, de modo que pueda usar bucles for mejorados si desea recorrerlo. (Mi Handclase también proporciona un get(int index)método, por lo tanto, Iterable<Card>está justificado en mi opinión).

La Iterableinterfaz proporciona lo siguiente (excluido javadoc):

public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Ahora puedes obtener una transmisión con:

Stream<Hand> stream = StreamSupport.stream(hand.spliterator(), false);

Entonces sobre la verdadera pregunta:

  • ¿Por qué Iterable<T>no proporciona un método predeterminado que implemente stream()y parallelStream()no veo nada que haga esto imposible o no deseado?

Sin embargo, una pregunta relacionada que encontré es la siguiente: ¿Por qué Stream <T> no implementa Iterable <T>?
Lo cual es curiosamente lo que sugiere que lo haga al revés.

skiwi
fuente
1
Supongo que esta es una buena pregunta para la Lista de Correo Lambda .
Edwin Dalorzo
¿Por qué es extraño querer iterar sobre una secuencia? ¿De qué otra forma podrías hacer break;una iteración? (Ok, Stream.findFirst()podría ser una solución, pero eso no puede satisfacer todas las necesidades ...)
glglgl
Vea también Convertir Iterable a Stream usando Java 8 JDK para soluciones prácticas.
Vadzim

Respuestas:

298

Esto no fue una omisión; hubo una discusión detallada sobre la lista de EG en junio de 2013.

La discusión definitiva del Grupo de Expertos se basa en este hilo .

Si bien parecía "obvio" (incluso para el Grupo de Expertos, inicialmente) que stream()parecía tener sentido Iterable, el hecho de que Iterablefuera tan general se convirtió en un problema, porque la firma obvia:

Stream<T> stream()

no siempre fue lo que ibas a querer. Algunas cosas que Iterable<Integer>preferirían que su método de flujo devuelva un IntStream, por ejemplo. Pero poner el stream()método tan alto en la jerarquía lo haría imposible. Entonces, en cambio, hicimos que sea realmente fácil hacer un a Streampartir de un Iterable, proporcionando un spliterator()método. La implementación de stream()in Collectiones solo:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

Cualquier cliente puede obtener la transmisión que desee de un Iterablecon:

Stream s = StreamSupport.stream(iter.spliterator(), false);

Al final concluimos que agregar stream()a Iterablesería un error.

Brian Goetz
fuente
8
Ya veo, en primer lugar, muchas gracias por responder la pregunta. Todavía tengo curiosidad por qué un Iterable<Integer>(creo que estás hablando?) Querría devolver un IntStream. ¿Lo iterable no sería más bien un PrimitiveIterator.OfInt? ¿O tal vez te refieres a otro caso de uso?
skiwi
139
Me parece extraño que la lógica anterior supuestamente se haya aplicado a Iterable (no puedo tener stream () porque alguien podría querer que devuelva IntStream), mientras que no se dio la misma cantidad de pensamiento para agregar exactamente el mismo método a Collection ( Es posible que desee que la secuencia de mi Colección <Integer> () también devuelva IntStream.) Ya sea que estuviera presente en ambos o ausente en ambos, las personas probablemente habrían seguido con sus vidas, pero porque está presente en uno y ausente en el otro, se convierte en una omisión evidente ...
Trejkaz
66
Brian McCutchon: Eso tiene más sentido para mí. Parece que la gente simplemente se cansó de discutir y decidió ir a lo seguro.
Jonathan Locke
44
Si bien esto tiene sentido, ¿hay alguna razón por la que no haya una alternativa estática Stream.of(Iterable), que al menos haría que el método fuera razonablemente reconocible al leer la documentación de la API? Como alguien que nunca ha trabajado realmente con las partes internas de Streams, nunca lo habría hecho. incluso mirado a StreamSupport, que se describe en la documentación de proporcionar "operaciones de bajo nivel" que "en su mayoría para los escritores de la biblioteca".
Julio
99
Estoy totalmente de acuerdo con Jules. se debe agregar un método estático Stream.of (Iteratable iter) o Stream.of (Iterator iter), en lugar de StreamSupport.stream (iter.spliterator (), falso);
user_3380739
23

Investigué en varias de las listas de correo lambda del proyecto y creo que encontré algunas discusiones interesantes.

No he encontrado una explicación satisfactoria hasta ahora. Después de leer todo esto, llegué a la conclusión de que era solo una omisión. Pero puede ver aquí que se discutió varias veces a lo largo de los años durante el diseño de la API.

Expertos en especificaciones de Lambda Libs

Encontré una discusión sobre esto en la lista de correo de expertos en especificaciones de Lambda Libs :

Bajo Iterable / Iterator.stream () Sam Pullara dijo:

Estaba trabajando con Brian para ver cómo se podría implementar la funcionalidad de límite / subtransmisión [1] y sugirió que la conversión a Iterator era la forma correcta de hacerlo. Había pensado en esa solución, pero no encontré ninguna forma obvia de tomar un iterador y convertirlo en una secuencia. Resulta que está allí, solo necesita convertir primero el iterador en un spliterator y luego convertir el spliterator en una secuencia. Esto me lleva a revisar si deberíamos tener estos colgando de Iterable / Iterator directamente o ambos.

Mi sugerencia es al menos tenerlo en Iterator para que pueda moverse limpiamente entre los dos mundos y también sería fácilmente detectable en lugar de tener que hacerlo:

Streams.stream (Spliterators.spliteratorUnknownSize (iterador, Spliterator.ORDERED))

Y luego Brian Goetz respondió :

Creo que el punto de Sam es que hay muchas clases de biblioteca que te dan un iterador pero no te dejan necesariamente escribir tu propio spliterator. Entonces, todo lo que puede hacer es llamar stream (spliteratorUnknownSize (iterator)). Sam sugiere que definamos Iterator.stream () para que lo haga por usted.

Me gustaría mantener los métodos stream () y spliterator () para escritores de bibliotecas / usuarios avanzados.

Y después

"Dado que escribir un Spliterator es más fácil que escribir un Iterator, preferiría escribir un Spliterator en lugar de un Iterator (Iterator es tan noventa :)".

Sin embargo, te estás perdiendo el punto. Hay millones de clases por ahí que ya te dan un iterador. Y muchos de ellos no están preparados para spliterator.

Discusiones anteriores en la lista de correo de Lambda

Puede que esta no sea la respuesta que está buscando, pero en la lista de correo del Proyecto Lambda esto se discutió brevemente. Quizás esto ayude a fomentar una discusión más amplia sobre el tema.

En palabras de Brian Goetz en Streams from Iterable :

Dar un paso atrás...

Hay muchas formas de crear un Stream. Cuanta más información tenga sobre cómo describir los elementos, más funcionalidad y rendimiento puede proporcionarle la biblioteca de secuencias. En orden de menor a mayor cantidad de información, son:

Iterador

Iterador + tamaño

Spliterator

Spliterator que conoce su tamaño

Spliterator que conoce su tamaño, y además sabe que todas las sub-divisiones conocen su tamaño.

(Algunos pueden sorprenderse al descubrir que podemos extraer paralelismo incluso de un iterador tonto en los casos en que Q (trabajo por elemento) no es trivial).

Si Iterable tuviera un método stream (), simplemente envolvería un iterador con un Spliterator, sin información de tamaño. Sin embargo, la mayoría de las cosas que son Iterable hacer tener información. Lo que significa que estamos sirviendo flujos deficientes. Eso no es tan bueno.

Una desventaja de la práctica API descrita aquí por Stephen, de aceptar Iterable en lugar de Collection, es que está forzando las cosas a través de una "pequeña tubería" y, por lo tanto, descarta la información de tamaño cuando puede ser útil. Está bien si todo lo que está haciendo es por Cada uno, pero si desea hacer más, es mejor si puede conservar toda la información que desea.

El valor predeterminado proporcionado por Iterable sería realmente malo: descartaría el tamaño a pesar de que la gran mayoría de los Iterables conocen esa información.

¿Contradicción?

Sin embargo, parece que la discusión se basa en los cambios que el Grupo de Expertos realizó en el diseño inicial de Streams, que inicialmente se basó en iteradores.

Aun así, es interesante notar que en una interfaz como Collection, el método de flujo se define como:

default Stream<E> stream() {
   return StreamSupport.stream(spliterator(), false);
}

Que podría ser exactamente el mismo código que se utiliza en la interfaz Iterable.

Entonces, es por eso que dije que esta respuesta probablemente no sea satisfactoria, pero aún así es interesante para la discusión.

Evidencia de refactorización

Continuando con el análisis en la lista de correo, parece que el método splitIterator estaba originalmente en la interfaz de la Colección, y en algún momento en 2013 lo movieron a Iterable.

Tire splitIterator hacia arriba de la colección a Iterable .

Conclusión / Teorías?

Entonces lo más probable es que la falta del método en Iterable sea solo una omisión, ya que parece que también deberían haber movido el método de flujo cuando movieron el SplitIterator de Collection a Iterable.

Si hay otras razones, esas no son evidentes. ¿Alguien más tiene otras teorías?

Edwin Dalorzo
fuente
Agradezco su respuesta, pero no estoy de acuerdo con el razonamiento allí. En el momento en que anula la función spliterator()de Iterable, entonces todos los problemas están solucionados, y puede implementar trivialmente stream()y parallelStream()..
skiwi
@skiwi Por eso dije que probablemente esta no sea la respuesta. Solo estoy tratando de agregar a la discusión, porque es difícil saber por qué el grupo de expertos tomó las decisiones que tomó. Supongo que todo lo que podemos hacer es tratar de hacer algunos análisis forenses en la lista de correo y ver si podemos encontrar alguna razón.
Edwin Dalorzo
1
@skiwi Revisé otras listas de correo y encontré más evidencia para la discusión y quizás algunas ideas que ayudan a teorizar algún diagnóstico.
Edwin Dalorzo
Gracias por sus esfuerzos, realmente debería aprender a dividir eficientemente esas listas de correo. Sería útil si pudieran visualizarse de alguna manera ... moderna, como un foro o algo así, porque leer correos electrónicos de texto sin formato con citas no es exactamente eficiente.
skiwi
6

Si conoce el tamaño que podría usar java.util.Collectionque proporciona el stream()método:

public class Hand extends AbstractCollection<Card> {
   private final List<Card> list = new ArrayList<>();
   private final int capacity;

   //...

   @Override
   public Iterator<Card> iterator() {
       return list.iterator();
   }

   @Override
   public int size() {
      return list.size();
   }
}

Y entonces:

new Hand().stream().map(...)

Enfrenté el mismo problema y me sorprendió que mi Iterableimplementación pudiera extenderse fácilmente a una AbstractCollectionimplementación simplemente agregando el size()método (por suerte tenía el tamaño de la colección :-)

También debe considerar anular Spliterator<E> spliterator().

Tú haces
fuente