web-dev-qa-db-fra.com

Devrais-je retourner une collection ou un flux?

Supposons que j'ai une méthode qui retourne 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 liste de lecture ou quelque chose. Le client ne pas stocke une référence à la liste pour une inspection ultérieure!

Étant donné ce scénario courant, devrais-je retourner un flux à la place?

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

Ou renvoie-t-il un flux non idiomatique en Java? Les flux ont-ils été conçus pour toujours être "terminés" dans la même expression dans laquelle ils ont été créés?

147
fredoverflow

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 utilisera probablement la réponse.

Tout d’abord, notez que vous pouvez toujours obtenir une collection à partir d’un flux, et inversement:

// 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 choix: Stream.

Si votre résultat est très grand, vous préférerez probablement Stream, car il ne sert à rien de le matérialiser en une fois et cela pourrait créer une pression considérable.

Si tout ce que l’appelant va faire, c’est itérer (recherche, filtre, agrégation), vous devriez préférer Stream, puisque Stream les intègre déjà et qu’il n’est pas nécessaire de matérialiser une collection (surtout si l’utilisateur risque de ne pas traiter le fichier). résultat entier.) Ceci est un cas très commun.

Même si vous savez que l'utilisateur le répètera plusieurs fois ou le conservera, vous voudrez peut-être retourner un flux à la place, pour le simple fait que la collection dans laquelle vous choisissez de le placer (par exemple, ArrayList) peut ne pas être la forme qu’ils veulent, et l’appelant doit le copier quand même. si vous renvoyez un flux, ils peuvent faire collect(toCollection(factory)) et le récupérer exactement sous la forme souhaitée.

Les cas ci-dessus de "préférence 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 de sa matérialisation dans une collection.

Le cas où vous devez renvoyer une collection se produit lorsqu'il existe de fortes exigences en matière de cohérence 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 solution - plus flexible, il n'impose pas de coûts de matérialisation inutiles et peut facilement être transformé en la Collection de votre choix si nécessaire. Mais parfois, vous devrez peut-être renvoyer une collection (en raison d'exigences de cohérence élevées, par exemple) ou vous voudrez peut-être renvoyer une collection car vous savez comment l'utilisateur l'utilisera et vous savez que c'est ce qui lui convient le mieux.

201
Brian Goetz

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

Il est assez courant de renvoyer un flux à partir d'un appel de méthode de style "getter". Voir le page d'utilisation du flux dans le fichier javadoc Java 8 et recherchez "des méthodes ... qui renvoient un flux" pour les packages autres que Java.util.Stream. Ces méthodes concernent généralement des classes qui représentent ou peuvent contenir plusieurs valeurs ou agrégations. Dans ce cas, les API en ont généralement renvoyé des collections ou des tableaux. Pour toutes les raisons indiquées par Brian dans sa réponse, il est très flexible d'ajouter Stream- méthodes de renvoi ici. Beaucoup de ces classes ont déjà des méthodes de collections ou de tableaux, car elles sont antérieures à l'API Streams. Si vous concevez une nouvelle API et qu'il est logique de fournir des méthodes de renvoi de flux, il se peut que ce ne soit pas nécessaire d'ajouter également des méthodes de retour de collection.

Brian a mentionné le coût de la "matérialisation" des valeurs dans une collection. Pour amplifier ce point, il y a en réalité deux coûts ici: le coût de stockage des valeurs dans la collection (allocation de mémoire et copie) et 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 paresse du Stream. Les API de Java.nio.file.Files En sont un bon exemple:

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

Non seulement readAllLines doit-il conserver le contenu entier du fichier en mémoire pour le stocker dans la liste des résultats, il doit également lire le fichier à la toute fin avant de renvoyer la liste. La méthode lines peut être retournée presque immédiatement après avoir effectué une configuration, laissant la lecture du fichier et les sauts de ligne à plus tard, le cas échéant - ou pas du tout. C'est un avantage énorme 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 entendu, 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 renvoi de flux après le pluriel du nom des objets qu’il représente ou contient, sans préfixe get. De même, alors que stream() est un nom raisonnable pour une méthode renvoyant un flux de données lorsqu'il n'y a qu'un seul ensemble de valeurs possible à renvoyer, il arrive parfois que des classes possèdent 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 renvoi de flux:

Stream<Attribute>  attributes();
Stream<Element>    elements();
62
Stuart Marks

Contrairement aux collections, les flux ont 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 par défaut partagé globalement pouvant avoir une incidence sur toute autre partie d'une application)
  • commandé ou non commandé

Ces différences existent aussi dans les collections, mais 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
  • Heureusement, la parallélisation n’est pas une préoccupation de la collection au-delà de la sécurité des threads.

En tant que consommateur d'un flux (à partir d'un retour de méthode ou en tant que paramètre de méthode), cette situation est dangereuse et source de confusion. Pour que leur algorithme se comporte correctement, les utilisateurs de flux doivent s’assurer qu’il n’exprime aucune hypothèse erronée quant aux 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 qu'ils soient 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) ...

Gardes des méthodes d'écriture pour les flux qui lève une exception IllegalArgumentException si le flux d'entrée a des caractéristiques qui altèrent votre algorithme est difficile, car les propriétés sont masquées.

Cela ne laisse que Stream comme choix valide dans une signature de méthode quand 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 de pool de threads), ce qui rend impossible le traitement accidentel de données avec des hypothèses erronées sur l'ordre, la taille ou la parallélisation (et l'utilisation du pool de threads).

1
tkruse

Les flux ont-ils été conçus pour toujours être "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: le retour d'un flux n'est pas si différent du retour d'un itérateur (admis avec beaucoup plus de pouvoir expressif)

IMHO la meilleure solution est d'encapsuler pourquoi vous faites cela et ne pas renvoyer la collection.

par exemple.

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

ou si vous avez l'intention de les compter

public int countPlayersWho(Predicate<? super Player> test);
1
Peter Lawrey

Si le flux est fini et qu'il existe une opération attendue/normale sur les objets renvoyés qui lève une exception vérifiée, je retourne toujours une collection. Parce que si vous allez faire quelque chose sur chacun des objets pouvant générer une exception de contrôle, vous détesterez le flux. Un manque réel avec les flux i il est impossible 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 elles sont parfois inévitables.

1
designbygravity

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

Il encourage également les utilisateurs de votre classe de domaine à écrire du code dans un style plus moderne Java 8. Il est possible de modifier progressivement ce style en conservant vos getters existants et en ajoutant de nouveaux getters renvoyant Stream. À tout moment, 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 est vraiment agréable une fois que vous avez effacé tout le code hérité!

0
Vazgen Torosyan

Je pense que cela dépend de votre scénario. Peut-être, si vous faites votre Team implémenter 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.

0
gontard