Soll ich eine Sammlung oder einen Stream zurückgeben?


163

Angenommen, ich habe eine Methode, die eine schreibgeschützte Ansicht in eine Mitgliederliste zurückgibt:

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

    // ...

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

Angenommen, der Client durchläuft die Liste nur einmal und sofort. Vielleicht, um die Spieler in eine JList zu setzen oder so. Der Kunde ist nicht speichert einen Verweis auf die Liste für eine spätere Inspektion!

Sollte ich in diesem allgemeinen Szenario stattdessen einen Stream zurückgeben?

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

Oder ist die Rückgabe eines Streams in Java nicht idiomatisch? Wurden Streams so konzipiert, dass sie immer innerhalb desselben Ausdrucks "beendet" werden, in dem sie erstellt wurden?


12
Daran ist definitiv nichts auszusetzen. Immerhin players.stream()ist genau eine solche Methode, die einen Stream an den Aufrufer zurückgibt. Die eigentliche Frage ist, möchten Sie den Anrufer wirklich auf eine einzelne Durchquerung beschränken und ihm auch den Zugriff auf Ihre Sammlung über die CollectionAPI verweigern ? Vielleicht möchte der Anrufer es einfach zu addAlleiner anderen Sammlung?
Marko Topolnik

2
Es hängt alles ab. Sie können immer collection.stream () sowie Stream.collect () ausführen. Es liegt also an Ihnen und dem Anrufer, der diese Funktion verwendet.
Raja Anbazhagan

Antworten:


222

Die Antwort lautet wie immer "es kommt darauf an". Dies hängt davon ab, wie groß die zurückgegebene Sammlung sein wird. Dies hängt davon ab, ob sich das Ergebnis im Laufe der Zeit ändert und wie wichtig die Konsistenz des zurückgegebenen Ergebnisses ist. Und es hängt sehr davon ab, wie der Benutzer die Antwort wahrscheinlich verwendet.

Beachten Sie zunächst, dass Sie immer eine Sammlung aus einem Stream abrufen können und umgekehrt:

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

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

Die Frage ist also, was für Ihre Anrufer nützlicher ist.

Wenn Ihr Ergebnis unendlich sein könnte, gibt es nur eine Wahl: Stream.

Wenn Ihr Ergebnis sehr groß sein könnte, bevorzugen Sie wahrscheinlich Stream, da es möglicherweise keinen Wert hat, alles auf einmal zu materialisieren, und dies könnte einen erheblichen Heap-Druck erzeugen.

Wenn der Aufrufer nur iterieren möchte (suchen, filtern, aggregieren), sollten Sie Stream bevorzugen, da Stream diese bereits integriert hat und keine Sammlung materialisiert werden muss (insbesondere, wenn der Benutzer die Sammlung möglicherweise nicht verarbeitet) ganzes Ergebnis.) Dies ist ein sehr häufiger Fall.

Selbst wenn Sie wissen, dass der Benutzer es mehrmals iterieren oder auf andere Weise beibehalten wird, möchten Sie möglicherweise stattdessen einen Stream zurückgeben, da die Sammlung, in die Sie ihn einfügen (z. B. ArrayList), möglicherweise nicht die ist Formular, das sie wollen, und dann muss der Anrufer es trotzdem kopieren. Wenn Sie einen Stream zurückgeben, können collect(toCollection(factory))sie ihn in genau der gewünschten Form abrufen.

Die oben genannten Fälle "Stream bevorzugen" ergeben sich hauptsächlich aus der Tatsache, dass Stream flexibler ist. Sie können sich spät daran binden, wie Sie es verwenden, ohne die Kosten und Einschränkungen für die Materialisierung in einer Sammlung zu verursachen.

Der einzige Fall, in dem Sie eine Sammlung zurückgeben müssen, besteht darin, dass hohe Konsistenzanforderungen bestehen und Sie einen konsistenten Schnappschuss eines sich bewegenden Ziels erstellen müssen. Dann möchten Sie die Elemente in eine Sammlung einfügen, die sich nicht ändert.

Daher würde ich sagen, dass Stream die meiste Zeit die richtige Antwort ist - es ist flexibler, verursacht normalerweise keine unnötigen Materialisierungskosten und kann bei Bedarf problemlos in die Sammlung Ihrer Wahl umgewandelt werden. Manchmal müssen Sie jedoch eine Sammlung zurückgeben (z. B. aufgrund starker Konsistenzanforderungen), oder Sie möchten die Sammlung zurückgeben, weil Sie wissen, wie der Benutzer sie verwenden wird, und wissen, dass dies für ihn am bequemsten ist.


6
Wie ich bereits sagte, gibt es einige Fälle, in denen es nicht fliegen kann, z. B. wenn Sie einen Schnappschuss in der Zeit eines sich bewegenden Ziels zurückgeben möchten, insbesondere wenn Sie hohe Konsistenzanforderungen haben. In den meisten Fällen scheint Stream jedoch die allgemeinere Wahl zu sein, es sei denn, Sie wissen etwas Bestimmtes darüber, wie es verwendet wird.
Brian Goetz

8
@ Marko Auch wenn Sie Ihre Frage so eng zusammenfassen, bin ich mit Ihrer Schlussfolgerung nicht einverstanden. Vielleicht nehmen Sie an, dass das Erstellen eines Streams viel teurer ist als das Umschließen der Sammlung mit einem unveränderlichen Wrapper? (Und selbst wenn Sie dies nicht tun, ist die Stream-Ansicht, die Sie auf dem Wrapper erhalten, schlechter als die, die Sie vom Original erhalten. Da UnmodizableList den Spliterator () nicht überschreibt, verlieren Sie effektiv jegliche Parallelität.) Fazit: Vorsicht der Vertrautheit Voreingenommenheit; Sie kennen Collection seit Jahren, und das könnte Sie dazu bringen, dem Neuling zu misstrauen.
Brian Goetz

5
@ MarkoTopolnik Sicher. Mein Ziel war es, die allgemeine Frage zum API-Design zu beantworten, die zu einer FAQ wird. Beachten Sie in Bezug auf die Kosten, dass das Materialisieren einer Sammlung in der Getter-Methode nicht billiger ist als das Zurückgeben eines Streams und das Vermieten , wenn Sie noch keine materialisierte Sammlung haben, die Sie zurückgeben oder verpacken können (OP tut dies, aber häufig gibt es keine) Der Anrufer materialisiert eine (und natürlich kann eine frühe Materialisierung viel teurer sein, wenn der Anrufer sie nicht benötigt oder wenn Sie ArrayList zurückgeben, der Anrufer jedoch TreeSet möchte.) Aber Stream ist neu und die Leute nehmen oft an, dass es mehr $$$ als ist es ist.
Brian Goetz

4
@MarkoTopolnik Während In-Memory ein sehr wichtiger Anwendungsfall ist, gibt es auch einige andere Fälle, die eine gute Parallelisierungsunterstützung bieten, z. B. nicht geordnete generierte Streams (z. B. Stream.generate). Wenn Streams jedoch schlecht passen, ist dies der reaktive Anwendungsfall, bei dem die Daten mit zufälliger Latenz ankommen. Dafür würde ich RxJava vorschlagen.
Brian Goetz

4
@MarkoTopolnik Ich glaube nicht, dass wir uns nicht einig sind, außer vielleicht, dass es uns gefallen hat, wenn wir unsere Bemühungen etwas anders fokussieren. (Wir sind daran gewöhnt; können nicht alle Menschen glücklich machen.) Das Design Center für Streams konzentrierte sich auf speicherinterne Datenstrukturen. Das Design Center für RxJava konzentriert sich auf extern generierte Ereignisse. Beide sind gute Bibliotheken; Außerdem schneiden beide nicht sehr gut ab, wenn Sie versuchen, sie auf Fälle anzuwenden, die außerhalb ihres Designzentrums liegen. Aber nur weil ein Hammer ein schreckliches Werkzeug für die Nadelspitze ist, heißt das nicht, dass mit dem Hammer etwas nicht stimmt.
Brian Goetz

63

Ich habe ein paar Punkte zu Brian Goetz 'hervorragender Antwort hinzuzufügen .

Es ist durchaus üblich, einen Stream von einem Methodenaufruf im "Getter" -Stil zurückzugeben. Besuchen Sie die Stream-Verwendungsseite im Java 8-Javadoc und suchen Sie nach "Methoden ..., die Stream zurückgeben" für andere Pakete als java.util.Stream. Diese Methoden beziehen sich normalerweise auf Klassen, die mehrere Werte oder Aggregationen von etwas darstellen oder enthalten können. In solchen Fällen haben APIs normalerweise Sammlungen oder Arrays davon zurückgegeben. Aus all den Gründen, die Brian in seiner Antwort erwähnt hat, ist es sehr flexibel, hier Methoden zur Rückgabe von Streams hinzuzufügen. Viele dieser Klassen verfügen bereits über Methoden zur Rückgabe von Sammlungen oder Arrays, da die Klassen älter sind als die Streams-API. Wenn Sie eine neue API entwerfen und es sinnvoll ist, Stream-Rückgabemethoden bereitzustellen, ist es möglicherweise nicht erforderlich, auch Sammlungsrückgabemethoden hinzuzufügen.

Brian erwähnte die Kosten für die "Materialisierung" der Werte in einer Sammlung. Um diesen Punkt zu verdeutlichen, fallen hier tatsächlich zwei Kosten an: die Kosten für das Speichern von Werten in der Sammlung (Speicherzuweisung und Kopieren) und auch die Kosten für das erstmalige Erstellen der Werte. Die letztgenannten Kosten können häufig reduziert oder vermieden werden, indem das faulheitssuchende Verhalten eines Streams ausgenutzt wird. Ein gutes Beispiel hierfür sind die APIs in java.nio.file.Files:

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

Es muss nicht nur readAllLinesden gesamten Dateiinhalt gespeichert werden, um ihn in der Ergebnisliste zu speichern, sondern die Datei muss auch bis zum Ende gelesen werden, bevor die Liste zurückgegeben wird. Die linesMethode kann fast sofort nach dem Einrichten zurückgegeben werden, sodass das Lesen von Dateien und Zeilenumbrüche erst später erfolgen, wenn dies erforderlich ist - oder überhaupt nicht. Dies ist ein großer Vorteil, wenn sich der Anrufer beispielsweise nur für die ersten zehn Zeilen interessiert:

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

Natürlich kann beträchtlicher Speicherplatz gespart werden, wenn der Anrufer den Stream filtert, um nur Zeilen zurückzugeben, die einem Muster usw. entsprechen.

Eine Redewendung, die sich abzuzeichnen scheint, besteht darin, Stream-Return-Methoden nach dem Plural des Namens der Dinge zu benennen, die sie darstellen oder enthalten, ohne getPräfix. Auch wenn stream()es ein vernünftiger Name für eine Stream-Rückgabemethode ist, wenn nur ein möglicher Satz von Werten zurückgegeben werden kann, gibt es manchmal Klassen mit Aggregationen mehrerer Wertetypen. Angenommen, Sie haben ein Objekt, das sowohl Attribute als auch Elemente enthält. Sie können zwei Stream-Return-APIs bereitstellen:

Stream<Attribute>  attributes();
Stream<Element>    elements();

3
Tolle Punkte. Können Sie mehr darüber sagen, wo diese Namenssprache entsteht und wie viel Traktion (Dampf?) Sie aufnimmt? Ich mag die Idee einer Namenskonvention, die deutlich macht, dass Sie einen Stream gegen eine Sammlung erhalten - obwohl ich auch oft erwarte, dass die IDE-Fertigstellung bei "get" mir sagt, was ich bekommen kann.
Joshua Goldberg

1
Ich bin auch sehr interessiert an dieser
wählen Sie den

5
@JoshuaGoldberg Das JDK scheint diese Namenssprache übernommen zu haben, wenn auch nicht ausschließlich. Beachten Sie: CharSequence.chars () und .codePoints (), BufferedReader.lines () und Files.lines () waren in Java 8 vorhanden. In Java 9 wurden folgende Elemente hinzugefügt: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Es wurden andere Stream-Return-Methoden hinzugefügt, die dieses Idiom nicht verwenden - Scanner.findAll () fällt mir ein -, aber das Plural-Nomen-Idiom scheint im JDK fair verwendet worden zu sein.
Stuart Marks

1

Wurden Streams so konzipiert, dass sie immer innerhalb desselben Ausdrucks "beendet" werden, in dem sie erstellt wurden?

So werden sie in den meisten Beispielen verwendet.

Hinweis: Die Rückgabe eines Streams unterscheidet sich nicht wesentlich von der Rückgabe eines Iterators (zugegeben mit viel mehr Ausdruckskraft).

IMHO ist die beste Lösung, zu beschreiben, warum Sie dies tun, und die Sammlung nicht zurückzugeben.

z.B

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

oder wenn Sie sie zählen wollen

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

2
Das Problem bei dieser Antwort ist, dass der Autor jede Aktion vorhersehen muss, die der Client ausführen möchte, und dass dies die Anzahl der Methoden in der Klasse erheblich erhöhen würde.
Dkatzel

@dkatzel Es hängt davon ab, ob der Endbenutzer der Autor oder jemand ist, mit dem er arbeitet. Wenn die Endbenutzer nicht bekannt sind, benötigen Sie eine allgemeinere Lösung. Möglicherweise möchten Sie den Zugriff auf die zugrunde liegende Sammlung weiterhin einschränken.
Peter Lawrey

1

Wenn der Stream endlich ist und für die zurückgegebenen Objekte eine erwartete / normale Operation ausgeführt wird, die eine aktivierte Ausnahme auslöst, gebe ich immer eine Sammlung zurück. Denn wenn Sie an jedem der Objekte etwas tun, das eine Prüfausnahme auslösen kann, hassen Sie den Stream. Ein wirklicher Mangel an Streams ist die Unfähigkeit, geprüfte Ausnahmen elegant zu behandeln.

Vielleicht ist das ein Zeichen dafür, dass Sie die überprüften Ausnahmen nicht benötigen, was fair ist, aber manchmal sind sie unvermeidlich.


1

Im Gegensatz zu Sammlungen weisen Streams zusätzliche Merkmale auf . Ein Stream, der von einer beliebigen Methode zurückgegeben wird, kann sein:

  • endlich oder unendlich
  • parallel oder sequentiell (mit einem standardmäßig global freigegebenen Threadpool, der sich auf jeden anderen Teil einer Anwendung auswirken kann)
  • bestellt oder nicht bestellt

Diese Unterschiede bestehen auch in Sammlungen, aber dort sind sie Teil des offensichtlichen Vertrags:

  • Alle Sammlungen haben Größe, Iterator / Iterable kann unendlich sein.
  • Sammlungen werden ausdrücklich bestellt oder nicht bestellt
  • Parallelität ist zum Glück nichts, worum sich die Sammlung über die Thread-Sicherheit hinaus kümmert.

Als Konsument eines Streams (entweder aus einer Methodenrückgabe oder als Methodenparameter) ist dies eine gefährliche und verwirrende Situation. Um sicherzustellen, dass sich ihr Algorithmus korrekt verhält, müssen Verbraucher von Streams sicherstellen, dass der Algorithmus keine falschen Annahmen über die Stream-Eigenschaften macht. Und das ist sehr schwer zu tun. Beim Unit-Test würde dies bedeuten, dass Sie alle Ihre Tests multiplizieren müssen, um sie mit demselben Stream-Inhalt zu wiederholen, jedoch mit Streams, die es sind

  • (endlich, geordnet, sequentiell)
  • (endlich, geordnet, parallel)
  • (endlich, ungeordnet, sequentiell) ...

Das Schreiben von Methodenschutz für Streams , die eine IllegalArgumentException auslösen, wenn der Eingabestream Merkmale aufweist, die Ihren Algorithmus beschädigen, ist schwierig, da die Eigenschaften ausgeblendet sind.

Damit bleibt Stream nur dann eine gültige Wahl in einer Methodensignatur, wenn keines der oben genannten Probleme von Bedeutung ist, was selten der Fall ist.

Es ist viel sicherer, andere Datentypen in Methodensignaturen mit einem expliziten Vertrag (und ohne implizite Thread-Pool-Verarbeitung) zu verwenden, der es unmöglich macht, versehentlich Daten mit falschen Annahmen über Reihenfolge, Größe oder Parallelität (und Threadpool-Verwendung) zu verarbeiten.


2
Ihre Bedenken hinsichtlich unendlicher Ströme sind unbegründet. Die Frage ist "sollte ich eine Sammlung oder einen Stream zurückgeben". Wenn Sammlung eine Möglichkeit ist, ist das Ergebnis per Definition endlich. Die Sorge, dass Anrufer eine unendliche Iteration riskieren könnten , da Sie eine Sammlung hätten zurückgeben können , ist unbegründet. Der Rest der Ratschläge in dieser Antwort ist nur schlecht. Es hört sich für mich so an, als hätten Sie jemanden getroffen, der Stream überbeansprucht hat, und Sie drehen sich zu stark in die andere Richtung. Verständliche, aber schlechte Ratschläge.
Brian Goetz

0

Ich denke, es hängt von Ihrem Szenario ab. TeamMöglicherweise Iterable<Player>ist es ausreichend , wenn Sie Ihr Gerät herstellen .

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

oder im a funktionalen Stil:

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

Wenn Sie jedoch eine vollständigere und flüssigere API wünschen, könnte ein Stream eine gute Lösung sein.


Beachten Sie, dass in dem vom OP veröffentlichten Code die Anzahl der Spieler fast unbrauchbar ist, außer als Schätzung ('1034 Spieler spielen jetzt, klicken Sie hier, um zu beginnen!'). Dies liegt daran, dass Sie eine unveränderliche Ansicht einer veränderlichen Sammlung zurückgeben Daher entspricht die Anzahl, die Sie jetzt erhalten, möglicherweise nicht der Anzahl in drei Mikrosekunden. Während die Rückgabe einer Sammlung Ihnen eine "einfache" Möglichkeit bietet, zur Zählung zu gelangen (und das stream.count()ist auch ziemlich einfach), ist diese Zahl für nichts anderes als das Debuggen oder Schätzen wirklich sehr bedeutsam.
Brian Goetz

0

Während einige der bekannteren Befragten großartige allgemeine Ratschläge gaben, bin ich überrascht, dass niemand genau gesagt hat:

Wenn Sie bereits ein "materialisiertes" Collectionin der Hand haben (dh es wurde bereits vor dem Aufruf erstellt - wie im angegebenen Beispiel, wo es sich um ein Mitgliedsfeld handelt), macht es keinen Sinn, es in ein zu konvertieren Stream. Der Anrufer kann dies problemlos selbst tun. Wenn der Anrufer die Daten in ihrer ursprünglichen Form verwenden möchte, werden Sie durch Konvertieren in Daten gezwungen, Streamredundante Arbeiten auszuführen, um eine Kopie der ursprünglichen Struktur wiederherzustellen.


-1

Vielleicht wäre eine Stream-Fabrik die bessere Wahl. Der große Vorteil, wenn Sammlungen nur über Stream verfügbar gemacht werden, besteht darin, dass die Datenstruktur Ihres Domain-Modells besser gekapselt wird. Es ist unmöglich, dass eine Verwendung Ihrer Domänenklassen das Innenleben Ihrer Liste oder Ihres Sets beeinflusst, indem Sie einfach einen Stream verfügbar machen.

Außerdem werden Benutzer Ihrer Domänenklasse dazu ermutigt, Code in einem moderneren Java 8-Stil zu schreiben. Es ist möglich, diesen Stil schrittweise umzugestalten, indem Sie Ihre vorhandenen Getter beibehalten und neue Get-Return-Getter hinzufügen. Im Laufe der Zeit können Sie Ihren Legacy-Code neu schreiben, bis Sie alle Getter gelöscht haben, die eine Liste oder einen Satz zurückgeben. Diese Art des Refactorings fühlt sich wirklich gut an, wenn Sie den gesamten Legacy-Code gelöscht haben!


7
Gibt es einen Grund, warum dies vollständig zitiert wird? Gibt es eine Quelle?
Xerus

-5

Ich hätte wahrscheinlich zwei Methoden, eine, um a zurückzugeben, Collectionund eine, um die Sammlung als zurückzugeben Stream.

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

// ...

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

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

}

Dies ist das Beste aus beiden Welten. Der Client kann wählen, ob er die Liste oder den Stream haben möchte, und er muss nicht die zusätzliche Objekterstellung durchführen, um eine unveränderliche Kopie der Liste zu erstellen, nur um einen Stream zu erhalten.

Dadurch wird Ihrer API nur eine weitere Methode hinzugefügt, sodass Sie nicht zu viele Methoden haben


1
Weil er zwischen diesen beiden Optionen wählen wollte und die Vor- und Nachteile jeder einzelnen fragte. Darüber hinaus bietet es jedem ein besseres Verständnis dieser Konzepte.
Libert Piou Piou

Bitte tu das nicht. Stellen Sie sich die APIs vor!
François Gautier
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.