Dois-je retourner une collection ou un flux?

163

Supposons que j'ai une méthode qui renvoie une vue en lecture seule dans une liste de membres:

class Team {
    private List < Player > players = new ArrayList < > ();

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}

Supposons en outre que tout ce que le client fait est de parcourir la liste une fois, immédiatement. Peut-être pour mettre les joueurs dans une JList ou quelque chose comme ça. Le client ne stocke pas de référence à la liste pour une inspection ultérieure!

Compte tenu de ce scénario courant, dois-je renvoyer un flux à la place?

public Stream < Player > getPlayers() {
    return players.stream();
}

Ou renvoyer un flux n'est-il pas idiomatique en Java? Les flux ont-ils été conçus pour être toujours «terminés» dans la même expression dans laquelle ils ont été créés?

fredoverflow
la source
12
Il n'y a certainement rien de mal à cela comme idiome. Après tout, players.stream()c'est une telle méthode qui renvoie un flux à l'appelant. La vraie question est la suivante: voulez-vous vraiment contraindre l'appelant à une traversée unique et lui refuser également l'accès à votre collection via l' CollectionAPI? Peut-être que l'appelant veut juste l'envoyer addAllà une autre collection?
Marko Topolnik
2
Tout dépend. Vous pouvez toujours faire collection.stream () ainsi que Stream.collect (). C'est donc à vous et à l'appelant qui utilise cette fonction.
Raja Anbazhagan

Réponses:

222

La réponse est, comme toujours, "ça dépend". Cela dépend de la taille de la collection retournée. Cela dépend de l'évolution du résultat dans le temps et de l'importance de la cohérence du résultat renvoyé. Et cela dépend beaucoup de la manière dont l'utilisateur est susceptible d'utiliser la réponse.

Tout d'abord, notez que vous pouvez toujours obtenir une collection à partir d'un flux, et vice versa:

// If API returns Collection, convert with stream()
getFoo().stream()...

// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

La question est donc de savoir ce qui est plus utile pour vos appelants.

Si votre résultat peut être infini, il n'y a qu'un seul choix: Stream.

Si votre résultat peut être très volumineux, vous préférez probablement Stream, car il peut ne pas être utile de tout matérialiser en même temps, ce qui pourrait créer une pression considérable sur le tas.

Si tout ce que l'appelant va faire est de le parcourir (recherche, filtre, agrégation), vous devriez préférer Stream, car Stream les a déjà intégrés et il n'est pas nécessaire de matérialiser une collection (surtout si l'utilisateur ne peut pas traiter le résultat global.) C'est un cas très courant.

Même si vous savez que l'utilisateur l'itérera plusieurs fois ou le gardera autrement, vous voudrez peut-être renvoyer un Stream à la place, pour le simple fait que quelle que soit la collection dans laquelle vous choisissez de la mettre (par exemple, ArrayList) peut ne pas être la forme qu'ils veulent, puis l'appelant doit le copier de toute façon. si vous renvoyez un flux, ils peuvent le faire collect(toCollection(factory))et l'obtenir exactement sous la forme souhaitée.

Les cas ci-dessus "prefer Stream" découlent principalement du fait que Stream est plus flexible; vous pouvez vous lier tardivement à la façon dont vous l'utilisez sans encourir les coûts et les contraintes liés à sa matérialisation dans une collection.

Le seul cas où vous devez renvoyer une collection est lorsqu'il existe des exigences de cohérence fortes et que vous devez produire un instantané cohérent d'une cible en mouvement. Ensuite, vous voudrez mettre les éléments dans une collection qui ne changera pas.

Je dirais donc que la plupart du temps, Stream est la bonne réponse - il est plus flexible, il n'impose pas de coûts de matérialisation généralement inutiles et peut être facilement transformé en collection de votre choix si nécessaire. Mais parfois, vous devrez peut-être retourner une collection (par exemple, en raison de fortes exigences de cohérence), ou vous voudrez peut-être renvoyer une collection car vous savez comment l'utilisateur l'utilisera et savez que c'est la chose la plus pratique pour eux.

Brian Goetz
la source
6
Comme je l'ai dit, il y a quelques cas où il ne volera pas, comme ceux où vous souhaitez renvoyer un instantané dans le temps d'une cible en mouvement, en particulier lorsque vous avez de fortes exigences de cohérence. Mais la plupart du temps, Stream semble le choix le plus général, à moins que vous ne sachiez quelque chose de spécifique sur la façon dont il sera utilisé.
Brian Goetz
8
@Marko Même si vous limitez votre question si étroitement, je ne suis toujours pas d'accord avec votre conclusion. Peut-être pensez-vous que la création d'un Stream est en quelque sorte beaucoup plus coûteuse que d'encapsuler la collection avec un wrapper immuable? (Et, même si vous ne le faites pas, la vue de flux que vous obtenez sur le wrapper est pire que ce que vous obtenez de l'original; comme UnmodifiableList ne remplace pas spliterator (), vous perdrez effectivement tout le parallélisme.) Conclusion: méfiez-vous de biais de familiarité; vous connaissez Collection depuis des années, et cela pourrait vous faire méfier du nouveau venu.
Brian Goetz
5
@MarkoTopolnik Bien sûr. Mon objectif était de répondre à la question générale de la conception des API, qui est en train de devenir une FAQ. En ce qui concerne le coût, notez que, si vous ne disposez pas déjà d'une collection matérialisée, vous pouvez la retourner ou l'envelopper (OP le fait, mais souvent il n'y en a pas), matérialiser une collection dans la méthode getter n'est pas moins chère que de renvoyer un flux et de la laisser l'appelant en matérialise un (et bien sûr, la matérialisation précoce peut être beaucoup plus coûteuse, si l'appelant n'en a pas besoin ou si vous renvoyez ArrayList mais l'appelant veut TreeSet.) Mais Stream est nouveau, et les gens supposent souvent que c'est plus de $$$ que c'est.
Brian Goetz
4
@MarkoTopolnik Bien que la mémoire en mémoire soit un cas d'utilisation très important, il existe également d'autres cas qui ont un bon support de parallélisation, comme les flux générés non ordonnés (par exemple, Stream.generate). Cependant, là où Streams ne convient pas, c'est le cas d'utilisation réactif, où les données arrivent avec une latence aléatoire. Pour cela, je suggérerais RxJava.
Brian Goetz
4
@MarkoTopolnik Je ne pense pas que nous soyons en désaccord, sauf peut-être que vous auriez peut-être aimé que nous concentrions nos efforts un peu différemment. (Nous sommes habitués à cela, nous ne pouvons pas rendre tout le monde heureux.) Le centre de conception de Streams s'est concentré sur les structures de données en mémoire; le centre de conception de RxJava se concentre sur les événements générés en externe. Les deux sont de bonnes bibliothèques; les deux ne se portent pas très bien lorsque vous essayez de les appliquer à des cas bien en dehors de leur centre de conception. Mais ce n'est pas parce qu'un marteau est un outil terrible pour la pointe de l'aiguille que cela ne signifie pas qu'il y a quelque chose qui ne va pas avec le marteau.
Brian Goetz
63

J'ai quelques points à ajouter à l'excellente réponse de Brian Goetz .

Il est assez courant de renvoyer un Stream à partir d'un appel de méthode de style "getter". Consultez la page d'utilisation de Stream dans Java 8 javadoc et recherchez les "méthodes ... qui retournent Stream" pour les packages autres que java.util.Stream. Ces méthodes sont généralement sur des classes qui représentent ou peuvent contenir plusieurs valeurs ou agrégations de quelque chose. Dans de tels cas, les API ont généralement renvoyé des collections ou des tableaux de celles-ci. Pour toutes les raisons que Brian a notées dans sa réponse, il est très flexible d'ajouter des méthodes de retour de flux ici. Beaucoup de ces classes ont déjà des méthodes de retour de collections ou de tableaux, car les classes sont antérieures à l'API Streams. Si vous concevez une nouvelle API et qu'il est judicieux de fournir des méthodes de retour de flux, il peut ne pas être nécessaire d'ajouter également des méthodes de retour de collection.

Brian a mentionné le coût de «matérialisation» des valeurs dans une collection. Pour amplifier ce point, il y a en fait deux coûts ici: le coût de stockage des valeurs dans la collection (allocation de mémoire et copie) et aussi le coût de création des valeurs en premier lieu. Ce dernier coût peut souvent être réduit ou évité en tirant parti du comportement de recherche de paresse d'un Stream. Un bon exemple de ceci sont les API dans java.nio.file.Files:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

Non seulement doit readAllLinesconserver le contenu entier du fichier en mémoire pour le stocker dans la liste de résultats, mais il doit également lire le fichier jusqu'à la toute fin avant qu'il ne renvoie la liste. La linesméthode peut revenir presque immédiatement après avoir effectué une configuration, laissant la lecture du fichier et le saut de ligne jusqu'à plus tard lorsque cela est nécessaire - ou pas du tout. C'est un énorme avantage, si par exemple, l'appelant ne s'intéresse qu'aux dix premières lignes:

try (Stream<String> lines = Files.lines(path)) {
    List<String> firstTen = lines.limit(10).collect(toList());
}

Bien sûr, un espace mémoire considérable peut être économisé si l'appelant filtre le flux pour ne renvoyer que les lignes correspondant à un modèle, etc.

Un idiome qui semble émerger est de nommer les méthodes de retour de flux après le pluriel du nom des choses qu'elles représentent ou contiennent, sans getpréfixe. De plus, bien que stream()soit un nom raisonnable pour une méthode de retour de flux lorsqu'il n'y a qu'un seul ensemble possible de valeurs à renvoyer, il existe parfois des classes qui ont des agrégations de plusieurs types de valeurs. Par exemple, supposons que vous ayez un objet contenant à la fois des attributs et des éléments. Vous pouvez fournir deux API de retour de flux:

Stream<Attribute>  attributes();
Stream<Element>    elements();
Marques Stuart
la source
3
Grands points. Pouvez-vous en dire plus sur l'endroit où vous voyez cet idiome de dénomination, et combien de traction (vapeur?) Il prend? J'aime l'idée d'une convention de nommage qui rend évident que vous obtenez un flux par rapport à une collection - même si j'attends aussi souvent que l'EDI complétée sur "get" me dise ce que je peux obtenir.
Joshua Goldberg
1
Je suis également très intéressé par cet idiome de nommage
élire le
5
@JoshuaGoldberg Le JDK semble avoir adopté cet idiome de dénomination, mais pas exclusivement. Considérez: CharSequence.chars () et .codePoints (), BufferedReader.lines () et Files.lines () existaient dans Java 8. Dans Java 9, les éléments suivants ont été ajoutés: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). D'autres méthodes de retour de flux ont été ajoutées qui n'utilisent pas cet idiome - Scanner.findAll () vient à l'esprit - mais l'idiome du nom pluriel semble avoir été utilisé dans le JDK.
Stuart marque le
1

Les flux ont-ils été conçus pour être toujours «terminés» dans la même expression dans laquelle ils ont été créés?

C'est ainsi qu'ils sont utilisés dans la plupart des exemples.

Remarque: renvoyer un Stream n'est pas si différent du retour d'un Iterator (admis avec une puissance beaucoup plus expressive)

À mon humble avis, la meilleure solution est d'encapsuler pourquoi vous faites cela et de ne pas renvoyer la collection.

par exemple

public int playerCount();
public Player player(int n);

ou si vous comptez les compter

public int countPlayersWho(Predicate<? super Player> test);
Peter Lawrey
la source
2
Le problème avec cette réponse est qu'elle obligerait l'auteur à anticiper chaque action que le client souhaite effectuer et qu'elle augmenterait considérablement le nombre de méthodes sur la classe.
dkatzel
@dkatzel Cela dépend si l'utilisateur final est l'auteur ou quelqu'un avec qui il travaille. Si les utilisateurs finaux sont inconnaissables, vous avez besoin d'une solution plus générale. Vous souhaiterez peut-être toujours limiter l'accès à la collection sous-jacente.
Peter Lawrey
1

Si le flux est fini et qu'il y a une opération attendue / normale sur les objets retournés qui lèvera une exception vérifiée, je retourne toujours une Collection. Parce que si vous allez faire quelque chose sur chacun des objets qui peuvent lever une exception de vérification, vous détesterez le flux. Un vrai manque avec les flux est l'incapacité de traiter les exceptions vérifiées avec élégance.

Maintenant, c'est peut-être un signe que vous n'avez pas besoin des exceptions vérifiées, ce qui est juste, mais parfois elles sont inévitables.

conception par gravité
la source
1

Contrairement aux collections, les flux ont des caractéristiques supplémentaires . Un flux renvoyé par n'importe quelle méthode peut être:

  • fini ou infini
  • parallèle ou séquentiel (avec un pool de threads partagé globalement par défaut qui peut avoir un impact sur toute autre partie d'une application)
  • commandé ou non commandé

Ces différences existent aussi dans les collections, mais là elles font partie du contrat évident:

  • Toutes les collections ont une taille, Iterator / Iterable peut être infini.
  • Les collections sont explicitement commandées ou non commandées
  • La parallélité n'est heureusement pas quelque chose dont la collection se soucie au-delà de la sécurité des threads.

En tant que consommateur d'un flux (provenant d'un retour de méthode ou d'un paramètre de méthode), c'est une situation dangereuse et déroutante. Pour s'assurer que leur algorithme se comporte correctement, les consommateurs de flux doivent s'assurer que l'algorithme ne fait pas d'hypothèse erronée sur les caractéristiques du flux. Et c'est une chose très difficile à faire. Dans les tests unitaires, cela signifierait que vous devez multiplier tous vos tests pour être répétés avec le même contenu de flux, mais avec des flux qui sont

  • (fini, ordonné, séquentiel)
  • (fini, ordonné, parallèle)
  • (fini, non ordonné, séquentiel) ...

Il est difficile d' écrire des méthodes de protection pour les flux qui lèvent une IllegalArgumentException si le flux d'entrée a des caractéristiques qui cassent votre algorithme, car les propriétés sont masquées.

Cela ne laisse à Stream qu'un choix valide dans une signature de méthode lorsque aucun des problèmes ci-dessus n'a d'importance, ce qui est rarement le cas.

Il est beaucoup plus sûr d'utiliser d'autres types de données dans les signatures de méthode avec un contrat explicite (et sans traitement implicite du pool de threads) qui rend impossible le traitement accidentel de données avec des hypothèses erronées sur l'ordre, la taille ou la parallélité (et l'utilisation du pool de threads).

tkruse
la source
2
Vos préoccupations concernant les flux infinis sont sans fondement; la question est "dois-je retourner une collection ou un flux". Si Collection est une possibilité, le résultat est par définition fini. Les craintes que les appelants risqueraient une itération infinie, étant donné que vous auriez pu renvoyer une collection , ne sont pas fondées. Le reste des conseils dans cette réponse est tout simplement mauvais. Il me semble que vous avez rencontré quelqu'un qui a surutilisé Stream et que vous faites une rotation excessive dans l'autre sens. Conseils compréhensibles, mais mauvais.
Brian Goetz
0

Je pense que cela dépend de votre scénario. Peut-être, si vous faites votre Teamoutil Iterable<Player>, c'est suffisant.

for (Player player : team) {
    System.out.println(player);
}

ou dans un style fonctionnel:

team.forEach(System.out::println);

Mais si vous voulez une API plus complète et plus fluide, un flux pourrait être une bonne solution.

Gontard
la source
Notez que, dans le code publié par l'OP, le nombre de joueurs est presque inutile, autre qu'une estimation ('1034 joueurs jouent maintenant, cliquez ici pour commencer!') C'est parce que vous retournez une vue immuable d'une collection mutable , donc le nombre que vous obtenez maintenant peut ne pas être égal au nombre de trois microsecondes à partir de maintenant. Donc, bien que renvoyer une Collection vous donne un moyen "facile" d'accéder au décompte (et c'est vraiment stream.count()assez facile aussi), ce nombre n'est pas vraiment très significatif pour autre chose que le débogage ou l'estimation.
Brian Goetz
0

Alors que certains des répondants les plus en vue ont donné d'excellents conseils généraux, je suis surpris que personne n'ait tout à fait déclaré:

Si vous avez déjà un "matérialisé" Collectionen main (c'est-à-dire qu'il a déjà été créé avant l'appel - comme c'est le cas dans l'exemple donné, où il s'agit d'un champ membre), il ne sert à rien de le convertir en un Stream. L'appelant peut facilement le faire lui-même. Alors que, si l'appelant veut consommer les données dans leur forme d'origine, vous les convertissez en un les Streamoblige à effectuer un travail redondant pour re-matérialiser une copie de la structure d'origine.

Daniel Avery
la source
-1

Peut-être qu'une usine Stream serait un meilleur choix. Le grand avantage de n'exposer que des collections via Stream est qu'il encapsule mieux la structure de données de votre modèle de domaine. Il est impossible pour toute utilisation de vos classes de domaine d'affecter le fonctionnement interne de votre liste ou ensemble simplement en exposant un flux.

Il encourage également les utilisateurs de votre classe de domaine à écrire du code dans un style Java 8 plus moderne. Il est possible de refactoriser progressivement ce style en conservant vos getters existants et en ajoutant de nouveaux getters de retour de flux. Au fil du temps, vous pouvez réécrire votre code hérité jusqu'à ce que vous ayez finalement supprimé tous les getters qui renvoient une liste ou un ensemble. Ce type de refactoring se sent vraiment bien une fois que vous avez effacé tout le code hérité!

Vazgen Torosyan
la source
7
y a-t-il une raison pour laquelle cela est entièrement cité? y a-t-il une source?
Xerus
-5

J'aurais probablement 2 méthodes, une pour retourner a Collectionet une pour retourner la collection en tant que Stream.

class Team
{
    private List<Player> players = new ArrayList<>();

// ...

    public List<Player> getPlayers()
    {
        return Collections.unmodifiableList(players);
    }

    public Stream<Player> getPlayerStream()
    {
        return players.stream();
    }

}

C'est le meilleur des deux mondes. Le client peut choisir s'il veut la liste ou le flux et il n'a pas à faire la création d'objet supplémentaire consistant à créer une copie immuable de la liste juste pour obtenir un flux.

Cela n'ajoute également qu'une seule méthode supplémentaire à votre API afin que vous n'ayez pas trop de méthodes

Dkatzel
la source
1
Parce qu'il voulait choisir entre ces deux options et a demandé le pour et le contre de chacune. De plus, il permet à chacun de mieux comprendre ces concepts.
Libert Piou Piou
Ne fais pas ça. Imaginez les API!
François Gautier