Pourquoi Iterable <T> ne fournit-il pas les méthodes stream () et parallelStream ()?

241

Je me demande pourquoi l' Iterableinterface ne fournit pas les méthodes stream()et parallelStream(). Considérez la classe suivante:

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();
    }
}

Il s'agit d'une implémentation d'une main car vous pouvez avoir des cartes en main tout en jouant à un jeu de cartes à collectionner.

Essentiellement, il enveloppe un List<Card>, assure une capacité maximale et offre d'autres fonctionnalités utiles. C'est mieux que de l'implémenter directement en tant que List<Card>.

Maintenant, pour plus de commodité, j'ai pensé que ce serait bien de l'implémenter Iterable<Card>, de sorte que vous pouvez utiliser des boucles for améliorées si vous voulez faire une boucle dessus. (Ma Handclasse fournit également une get(int index)méthode, d'où la Iterable<Card>justification à mon avis.)

L' Iterableinterface fournit les éléments suivants (javadoc omis):

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);
    }
}

Maintenant, pouvez-vous obtenir un flux avec:

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

Donc, sur la vraie question:

  • Pourquoi ne Iterable<T>fournit pas de méthodes par défaut qui implémentent stream()et parallelStream(), je ne vois rien qui rendrait cela impossible ou indésirable?

Une question connexe que j'ai trouvée est la suivante: pourquoi Stream <T> n'implémente-t-il pas Iterable <T>?
Ce qui est assez curieusement suggérant de le faire un peu l'inverse.

skiwi
la source
1
Je suppose que c'est une bonne question pour la liste de diffusion Lambda .
Edwin Dalorzo
Pourquoi est-il étrange de vouloir parcourir un flux? Sinon, comment pourriez-vous éventuellement break;une itération? (D'accord, cela Stream.findFirst()pourrait être une solution, mais cela pourrait ne pas répondre à tous les besoins ...)
glglgl
Voir également Convertir Iterable en Stream à l'aide de Java 8 JDK pour des solutions pratiques.
Vadzim

Réponses:

298

Ce n'était pas une omission; il y a eu une discussion détaillée sur la liste EG en juin 2013.

La discussion définitive du Groupe d'experts est enracinée dans ce fil .

S'il est apparu "évident" (même au Groupe d'experts, au départ) que stream() semblait logique Iterable, le fait que ce Iterablesoit si général devenait un problème, car la signature évidente:

Stream<T> stream()

n'était pas toujours ce que vous vouliez. Certaines choses qui Iterable<Integer>auraient préféré que leur méthode stream renvoie un IntStream, par exemple. Mais mettre la stream()méthode aussi haut dans la hiérarchie rendrait cela impossible. Au lieu de cela, nous avons rendu très facile la création Streamd'unIterable , en fournissant une spliterator()méthode. L'implémentation de stream()in Collectionest juste:

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

N'importe quel client peut obtenir le flux de son choix Iterableavec:

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

En fin de compte, nous avons conclu que l'ajout stream()de Iterableserait une erreur.

Brian Goetz
la source
8
Je vois tout d'abord merci beaucoup d'avoir répondu à la question. Je suis toujours curieux de savoir pourquoi un Iterable<Integer>(je pense que vous parlez?) Voudrait retourner un IntStream. L'itérable ne serait-il pas alors plutôt un PrimitiveIterator.OfInt? Ou voulez-vous dire peut-être un autre cas d'utilisation?
skiwi
139
Je trouve étrange que la logique ci-dessus ait été censée être appliquée à Iterable (je ne peux pas avoir stream () car quelqu'un pourrait vouloir qu'il retourne IntStream) alors qu'une quantité égale de réflexion n'a pas été accordée à l'ajout de la même méthode exacte à Collection ( Je pourrais vouloir que le flux () de ma collection <Integer> retourne également IntStream.) Qu'il soit présent sur les deux ou absent sur les deux, les gens auraient probablement juste commencé leur vie, mais parce qu'il est présent sur un et absent sur l'autre, ça devient une omission flagrante ...
Trejkaz
6
Brian McCutchon: Cela me semble plus logique. On dirait que les gens se sont lassés de se disputer et ont décidé de jouer prudemment.
Jonathan Locke
44
Bien que cela ait du sens, y a-t-il une raison pour laquelle il n'y a pas d'alternative statique Stream.of(Iterable), ce qui rendrait au moins la méthode raisonnablement détectable en lisant la documentation de l'API - comme quelqu'un qui n'a jamais vraiment travaillé avec les composants internes de Streams, je ne l'aurais jamais fait même regardé à StreamSupport, qui est décrit dans la documentation en fournissant des « opérations de bas niveau » qui sont « la plupart du temps pour les écrivains bibliothèque ».
Jules
9
Je suis totalement d'accord avec Jules. une méthode statique Stream.of (itérateur itératif) ou Stream.of (itérateur itérateur) doit être ajoutée, au lieu de StreamSupport.stream (iter.spliterator (), false);
user_3380739
23

J'ai fait une enquête dans plusieurs des listes de diffusion du projet lambda et je pense avoir trouvé quelques discussions intéressantes.

Je n'ai jusqu'à présent pas trouvé d'explication satisfaisante. Après avoir lu tout cela, j'ai conclu que c'était juste une omission. Mais vous pouvez voir ici qu'il a été discuté plusieurs fois au cours des années lors de la conception de l'API.

Experts en spécifications Lambda Libs

J'ai trouvé une discussion à ce sujet dans la liste de diffusion Lambda Libs Spec Experts :

Sous Iterable / Iterator.stream (), Sam Pullara a déclaré:

Je travaillais avec Brian pour voir comment la fonctionnalité de limite / sous-flux [1] pourrait être implémentée et il a suggéré que la conversion vers Iterator était la bonne façon de procéder. J'avais pensé à cette solution mais je n'ai trouvé aucun moyen évident de prendre un itérateur et de le transformer en un flux. Il s'avère qu'il est là, il vous suffit de convertir d'abord l'itérateur en séparateur, puis de convertir le séparateur en flux. Cela m'amène donc à revenir sur la question de savoir si nous devrions les suspendre directement à Iterable / Iterator ou aux deux.

Ma suggestion est de l'avoir au moins sur Iterator afin que vous puissiez vous déplacer proprement entre les deux mondes et il serait également facilement détectable plutôt que d'avoir à le faire:

Streams.stream (Spliterators.spliteratorUnknownSize (itérateur, Spliterator.ORDERED))

Et puis Brian Goetz a répondu :

Je pense que le point de Sam était qu'il existe de nombreuses classes de bibliothèque qui vous donnent un itérateur mais ne vous permettent pas nécessairement d'écrire votre propre séparateur. Donc, tout ce que vous pouvez faire est d'appeler le flux (spliteratorUnknownSize (itérateur)). Sam suggère que nous définissions Iterator.stream () pour le faire pour vous.

Je voudrais conserver les méthodes stream () et spliterator () comme étant destinées aux rédacteurs de bibliothèque / utilisateurs avancés.

Et ensuite

"Étant donné qu'écrire un Spliterator est plus facile que d'écrire un Iterator, je préférerais simplement écrire un Spliterator au lieu d'un Iterator (Iterator a tellement 90 ans :)"

Vous manquez le point, cependant. Il y a des millions de classes qui vous offrent déjà un itérateur. Et beaucoup d'entre eux ne sont pas prêts pour le séparateur.

Discussions précédentes dans la liste de diffusion Lambda

Ce n'est peut-être pas la réponse que vous recherchez, mais dans la liste de diffusion Project Lambda cela a été brièvement discuté. Cela peut peut-être favoriser une discussion plus large sur le sujet.

Pour reprendre les mots de Brian Goetz sous Streams from Iterable :

Reculer...

Il existe de nombreuses façons de créer un flux. Plus vous avez d'informations sur la description des éléments, plus la bibliothèque de flux peut vous offrir de fonctionnalités et de performances. Dans l'ordre du moins à la plupart des informations, ils sont:

Itérateur

Itérateur + taille

Spliterator

Spliterator qui connaît sa taille

Séparateur qui connaît sa taille et sait en outre que toutes les sous-divisions connaissent leur taille.

(Certains peuvent être surpris de constater que nous pouvons extraire le parallélisme même à partir d'un itérateur muet dans les cas où Q (travail par élément) n'est pas trivial.)

Si Iterable avait une méthode stream (), il envelopperait simplement un Iterator avec un Spliterator, sans aucune information de taille. Mais, la plupart des choses qui sont Iterable faire des informations de taille. Ce qui signifie que nous servons des flux déficients. Ce n'est pas si bon.

Un inconvénient de la pratique de l'API décrite par Stephen ici, d'accepter Iterable au lieu de Collection, est que vous forcez les choses à travers un "petit tuyau" et que vous jetez donc les informations de taille lorsqu'elles pourraient être utiles. C'est bien si tout ce que vous faites est pour chacun d'eux, mais si vous voulez en faire plus, c'est mieux si vous pouvez conserver toutes les informations que vous voulez.

La valeur par défaut fournie par Iterable serait vraiment minable - elle éliminerait la taille même si la grande majorité des Iterables connaissent cette information.

Contradiction?

Cependant, il semble que la discussion soit basée sur les changements que le Groupe d'experts a apportés à la conception initiale des Streams qui était initialement basée sur des itérateurs.

Même ainsi, il est intéressant de noter que dans une interface comme Collection, la méthode stream est définie comme:

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

Ce qui pourrait être exactement le même code utilisé dans l'interface Iterable.

C'est pourquoi j'ai dit que cette réponse n'est probablement pas satisfaisante, mais toujours intéressante pour la discussion.

Preuve de refactorisation

Poursuivant l'analyse dans la liste de diffusion, il semble que la méthode splitIterator était à l'origine dans l'interface Collection, et à un moment donné en 2013, ils l'ont déplacée vers Iterable.

Tirez splitIterator de Collection à Iterable .

Conclusion / Théories?

Ensuite, il est probable que l'absence de la méthode dans Iterable ne soit qu'une omission, car il semble qu'ils auraient également dû déplacer la méthode de flux lorsqu'ils ont déplacé le splitIterator de Collection à Iterable.

S'il y a d'autres raisons, celles-ci ne sont pas évidentes. Quelqu'un d'autre a d'autres théories?

Edwin Dalorzo
la source
J'apprécie votre réponse, mais je ne suis pas d'accord avec le raisonnement là-bas. Au moment où vous remplacez le spliterator()la Iterable, tous les problèmes sont fixés, et vous pouvez trivialement mettre en œuvre stream()et parallelStream()..
skiwi
@skiwi C'est pourquoi j'ai dit que ce n'était probablement pas la réponse. J'essaie simplement d'ajouter à la discussion, car il est difficile de savoir pourquoi le groupe d'experts a pris les décisions qu'il a prises. J'imagine que tout ce que nous pouvons faire, c'est essayer de faire quelques analyses médico-légales dans la liste de diffusion et voir si nous pouvons trouver des raisons.
Edwin Dalorzo
1
@skiwi J'ai examiné d'autres listes de diffusion et trouvé plus de preuves pour la discussion et peut-être quelques idées qui aident à théoriser un diagnostic.
Edwin Dalorzo
Merci pour vos efforts, je devrais vraiment apprendre à diviser efficacement ces listes de diffusion. Il serait utile qu'ils puissent être visualisés d'une manière ... moderne, comme un forum ou quelque chose, car lire des e-mails en texte brut avec des citations n'est pas exactement efficace.
skiwi
6

Si vous connaissez la taille que vous pourriez utiliser et java.util.Collectionqui fournit la stream()méthode:

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();
   }
}

Puis:

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

J'ai rencontré le même problème et j'ai été surpris que mon Iterableimplémentation puisse être très facilement étendue à une AbstractCollectionimplémentation en ajoutant simplement lesize() méthode (heureusement, j'avais la taille de la collection :-)

Vous devez également envisager de remplacer Spliterator<E> spliterator().

Tu fais
la source