Devo restituire una raccolta o uno stream?


163

Supponiamo di avere un metodo che restituisce una vista di sola lettura in un elenco di membri:

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

    // ...

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

Supponiamo inoltre che tutto il client faccia l'iterazione dell'elenco una volta, immediatamente. Forse per mettere i giocatori in una JList o qualcosa del genere. Il client non memorizza un riferimento all'elenco per ispezioni successive!

Dato questo scenario comune, dovrei invece restituire uno stream?

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

O la restituzione di uno stream non è idiomatica in Java? Gli stream sono stati progettati per essere "terminati" sempre all'interno della stessa espressione in cui sono stati creati?


12
Non c'è assolutamente nulla di sbagliato in questo come un linguaggio. Dopotutto, players.stream()è proprio un tale metodo che restituisce un flusso al chiamante. La vera domanda è: vuoi davvero vincolare il chiamante al singolo attraversamento e negargli anche l'accesso alla tua raccolta tramite l' CollectionAPI? Forse il chiamante vuole semplicemente addAllfarlo in un'altra raccolta?
Marko Topolnik,

2
Tutto dipende. Puoi sempre fare collection.stream () e Stream.collect (). Quindi tocca a te e al chiamante che usa quella funzione.
Raja Anbazhagan,

Risposte:


222

La risposta è, come sempre, "dipende". Dipende da quanto sarà grande la collezione restituita. Dipende se il risultato cambia nel tempo e dall'importanza della coerenza del risultato restituito. E dipende molto da come è probabile che l'utente usi la risposta.

Innanzitutto, tieni presente che puoi sempre ottenere una raccolta da uno stream e viceversa:

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

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

Quindi la domanda è, che è più utile per i tuoi chiamanti.

Se il tuo risultato potrebbe essere infinito, c'è solo una scelta: Stream.

Se il tuo risultato potrebbe essere molto grande, probabilmente preferisci Stream, dal momento che potrebbe non esserci alcun valore nel materializzarlo tutto in una volta, e ciò potrebbe creare una pressione di heap significativa.

Se tutto ciò che il chiamante sta per fare è scorrere attraverso di esso (ricerca, filtro, aggregazione), dovresti preferire Stream, poiché Stream ha già questi built-in e non è necessario materializzare una raccolta (soprattutto se l'utente potrebbe non elaborare il risultato complessivo.) Questo è un caso molto comune.

Anche se sai che l'utente lo ripeterà più volte o lo manterrà in altro modo, potresti comunque voler restituire uno Stream, per il semplice fatto che qualunque raccolta tu scelga di inserirlo (ad esempio, ArrayList) potrebbe non essere il modulo che vogliono, quindi il chiamante deve copiarlo comunque. se restituisci un flusso, possono farlo collect(toCollection(factory))e ottenerlo esattamente nella forma che desiderano.

I suddetti casi "preferisci Stream" derivano principalmente dal fatto che Stream è più flessibile; puoi legarti in ritardo a come lo usi senza incorrere in costi e vincoli per materializzarlo in una Collezione.

L'unico caso in cui è necessario restituire una raccolta è quando vi sono forti requisiti di coerenza e si deve produrre un'istantanea coerente di un bersaglio in movimento. Quindi, vorrai inserire gli elementi in una raccolta che non cambierà.

Quindi direi che la maggior parte delle volte Stream è la risposta giusta: è più flessibile, non impone costi di materializzazione solitamente non necessari e, se necessario, può essere facilmente trasformato nella Collezione di tua scelta. A volte, tuttavia, potrebbe essere necessario restituire una raccolta (ad esempio, a causa di requisiti di coerenza elevati) oppure è possibile restituire la raccolta perché si sa come l'utente la utilizzerà e si sa che questa è la cosa più conveniente per loro.


6
Come ho detto, ci sono alcuni casi in cui non volerà, come quelli in cui si desidera restituire un'istantanea nel tempo di un bersaglio in movimento, soprattutto quando si hanno requisiti di coerenza elevati. Ma la maggior parte delle volte, Stream sembra la scelta più generale, a meno che tu non sappia qualcosa di specifico su come verrà utilizzato.
Brian Goetz,

8
@Marko Anche se confini la tua domanda così strettamente, non sono ancora d'accordo con la tua conclusione. Forse stai supponendo che la creazione di uno stream sia in qualche modo molto più costosa rispetto al confezionamento della raccolta con un wrapper immutabile? (E, anche se non lo fai, la visualizzazione dello stream che ottieni sul wrapper è peggiore di quella che ottieni dall'originale; poiché UnmodifiableList non ignora lo spliterator (), perderai effettivamente tutto il parallelismo.) In conclusione: attenzione di parzialità familiare; conosci Collezione da anni e questo potrebbe farti sfidare il nuovo arrivato.
Brian Goetz,

5
@MarkoTopolnik Certo. Il mio obiettivo era quello di rispondere alla domanda generale sulla progettazione delle API, che sta diventando una FAQ. Per quanto riguarda i costi, tieni presente che, se non disponi già di una raccolta materializzata, puoi restituire o avvolgere (OP fa, ma spesso non ce n'è una), materializzare una raccolta nel metodo getter non è più economico che restituire un flusso e lasciare il chiamante ne materializza uno (e ovviamente la materializzazione precoce potrebbe essere molto più costosa, se il chiamante non ne ha bisogno o se si restituisce ArrayList ma il chiamante vuole TreeSet.) Ma Stream è nuovo e le persone spesso assumono più di è.
Brian Goetz,

4
@MarkoTopolnik Mentre in-memory è un caso d'uso molto importante, ci sono anche alcuni altri casi che hanno un buon supporto di parallelizzazione, come flussi generati non ordinati (ad esempio Stream.generate). Tuttavia, dove Streams è inadeguato è il caso d'uso reattivo, in cui i dati arrivano con latenza casuale. Per questo, suggerirei RxJava.
Brian Goetz

4
@MarkoTopolnik Non credo che siamo in disaccordo, tranne forse per il fatto che ti sarebbe piaciuto che noi concentrassimo i nostri sforzi in modo leggermente diverso. (Siamo abituati a questo; non possiamo rendere felici tutte le persone.) Il centro di progettazione per Stream si è concentrato su strutture di dati in memoria; il centro di progettazione per RxJava si concentra su eventi generati esternamente. Entrambe sono buone biblioteche; inoltre, entrambi non vanno molto bene quando provi ad applicarli a casi ben fuori dal loro centro di progettazione. Ma solo perché un martello è uno strumento terribile per il ricamo ad ago, ciò non suggerisce che ci sia qualcosa di sbagliato nel martello.
Brian Goetz,

63

Ho alcuni punti da aggiungere all'ottima risposta di Brian Goetz .

È abbastanza comune restituire uno stream da una chiamata del metodo di stile "getter". Vedere la pagina di utilizzo dello Stream nel javadoc di Java 8 e cercare "metodi ... che restituiscono Stream" per i pacchetti diversi da java.util.Stream. Questi metodi sono generalmente su classi che rappresentano o possono contenere più valori o aggregazioni di qualcosa. In tali casi, le API in genere hanno restituito raccolte o array di esse. Per tutti i motivi che Brian ha notato nella sua risposta, è molto flessibile aggiungere qui i metodi di ritorno dello stream. Molte di queste classi hanno già metodi di restituzione delle raccolte o dell'array, poiché le classi sono precedenti all'API Streams. Se stai progettando una nuova API e ha senso fornire metodi di restituzione dello stream, potrebbe non essere necessario aggiungere anche metodi di restituzione della raccolta.

Brian ha menzionato il costo di "materializzare" i valori in una raccolta. Per amplificare questo punto, in realtà ci sono due costi qui: il costo della memorizzazione dei valori nella raccolta (allocazione della memoria e copia) e anche il costo della creazione dei valori in primo luogo. Quest'ultimo costo può spesso essere ridotto o evitato sfruttando il comportamento di una pigrizia di Stream. Un buon esempio di questo sono le API injava.nio.file.Files :

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

Non solo readAllLinesdeve contenere l'intero contenuto del file in memoria per poterlo archiviare nell'elenco dei risultati, ma deve anche leggere il file fino alla fine prima di restituire l'elenco. Il linesmetodo può tornare quasi immediatamente dopo aver eseguito alcune impostazioni, lasciando la lettura dei file e l'interruzione di riga fino a quando è necessario - o per niente. Questo è un enorme vantaggio, se ad esempio il chiamante è interessato solo alle prime dieci linee:

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

Naturalmente è possibile risparmiare un notevole spazio di memoria se il chiamante filtra il flusso per restituire solo le linee corrispondenti a un modello, ecc.

Un idioma che sembra emergere è nominare i metodi di ritorno del flusso dopo il plurale del nome delle cose che rappresenta o contiene, senza getprefisso. Inoltre, sebbene stream()sia un nome ragionevole per un metodo di ritorno dello stream quando è possibile restituire un solo set di valori, a volte ci sono classi che hanno aggregazioni di più tipi di valori. Ad esempio, supponiamo di avere qualche oggetto che contiene sia attributi che elementi. Potresti fornire due API di ritorno dello stream:

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

3
Grandi punti. Puoi dire di più su dove stai vedendo sorgere quell'idioma di denominazione e quanta trazione (vapore?) Sta raccogliendo? Mi piace l'idea di una convenzione di denominazione che renda evidente che stai ricevendo un flusso rispetto a una raccolta, anche se spesso mi aspetto anche il completamento dell'IDE su "get" per dirmi cosa posso ottenere.
Joshua Goldberg,

1
Sono anche molto interessato a quell'idioma di denominazione
eleggere il

5
@JoshuaGoldberg Il JDK sembra aver adottato questo idioma di denominazione, sebbene non esclusivamente. Considerare: CharSequence.chars () e .codePoints (), BufferedReader.lines () e Files.lines () esistevano in Java 8. In Java 9 sono stati aggiunti i seguenti: Process.children (), NetworkInterface.addresses ( ), Scanner.tokens (), Matcher.results (), java.xml.catalog.Catalog.catalogs (). Sono stati aggiunti altri metodi di ritorno dello stream che non usano questo idioma - mi viene in mente Scanner.findAll () - ma il linguaggio plurale del sostantivo sembra essere entrato in uso nel JDK.
Stuart segna il

1

Gli stream sono stati progettati per essere "terminati" sempre all'interno della stessa espressione in cui sono stati creati?

Ecco come vengono utilizzati nella maggior parte degli esempi.

Nota: la restituzione di uno Stream non è diversa dalla restituzione di un Iteratore (ammesso con un potere espressivo molto maggiore)

IMHO la soluzione migliore è incapsulare il motivo per cui lo stai facendo e non restituire la raccolta.

per esempio

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

o se hai intenzione di contarli

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

2
Il problema con questa risposta è che richiederebbe all'autore di anticipare ogni azione che il cliente vuole fare e aumenterebbe notevolmente il numero di metodi sulla classe.
dkatzel,

@dkatzel Dipende se l'utente finale è l'autore o qualcuno con cui lavora. Se gli utenti finali sono inconoscibili, è necessaria una soluzione più generale. Potresti comunque voler limitare l'accesso alla raccolta sottostante.
Peter Lawrey,

1

Se il flusso è finito e c'è un'operazione prevista / normale sugli oggetti restituiti che genererà un'eccezione controllata, restituisco sempre una raccolta. Perché se stai per fare qualcosa su ciascuno degli oggetti che possono generare un'eccezione di controllo, odierai il flusso. Una vera mancanza di flussi in grado di gestire le eccezioni verificate in modo elegante.

Ora, forse questo è un segno che non hai bisogno delle eccezioni verificate, il che è giusto, ma a volte sono inevitabili.


1

A differenza delle raccolte, i flussi hanno caratteristiche aggiuntive . Un flusso restituito con qualsiasi metodo potrebbe essere:

  • finito o infinito
  • parallelo o sequenziale (con un pool di thread condiviso a livello globale predefinito che può influire su qualsiasi altra parte di un'applicazione)
  • ordinato o non ordinato

Queste differenze esistono anche nelle collezioni, ma lì fanno parte dell'ovvio contratto:

  • Tutte le raccolte hanno dimensioni, Iteratore / Iterabile possono essere infinite.
  • Le raccolte sono esplicitamente ordinate o non ordinate
  • Per fortuna, la parallelismo non è qualcosa di cui la collezione si preoccupa oltre la sicurezza dei thread.

Come consumatore di un flusso (o da un ritorno di metodo o come parametro di metodo) questa è una situazione pericolosa e confusa. Per assicurarsi che il loro algoritmo si comporti correttamente, i consumatori di stream devono assicurarsi che l'algoritmo non faccia ipotesi errate sulle caratteristiche del flusso. E questa è una cosa molto difficile da fare. Nel test unitario, ciò significherebbe che è necessario moltiplicare tutti i test per essere ripetuti con gli stessi contenuti del flusso, ma con flussi che sono

  • (finito, ordinato, sequenziale)
  • (finito, ordinato, parallelo)
  • (finito, non ordinato, sequenziale) ...

Protezione del metodo di scrittura per i flussi che generano un'eccezione IllegalArgumentException se il flusso di input ha caratteristiche che rompono l'algoritmo è difficile, perché le proprietà sono nascoste.

Ciò lascia Stream solo come una scelta valida in una firma del metodo quando nessuno dei problemi sopra elencati è importante, cosa che accade raramente.

È molto più sicuro utilizzare altri tipi di dati nelle firme dei metodi con un contratto esplicito (e senza l'elaborazione implicita del pool di thread) che rende impossibile elaborare accidentalmente i dati con ipotesi errate su ordinanza, dimensioni o parallelità (e utilizzo del thread pool).


2
Le tue preoccupazioni per i flussi infiniti sono infondate; la domanda è "dovrei restituire una raccolta o uno stream". Se la raccolta è una possibilità, il risultato è per definizione finito. Quindi le preoccupazioni che i chiamanti rischierebbero un'iterazione infinita, dato che potresti aver restituito una raccolta , sono infondate. Il resto dei consigli in questa risposta è semplicemente negativo. Mi sembra che ti sia imbattuto in qualcuno che ha usato troppo Stream e che stai ruotando troppo nell'altra direzione. Comprensibile, ma cattivo consiglio.
Brian Goetz,

0

Penso che dipenda dal tuo scenario. Può essere, se fai il tuo Teamattrezzo Iterable<Player>, è sufficiente.

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

o in uno stile funzionale:

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

Ma se vuoi un'API più completa e fluente, uno stream potrebbe essere una buona soluzione.


Nota che, nel codice pubblicato dall'OP, il conteggio dei giocatori è quasi inutile, a parte una stima ("1034 giocatori stanno giocando ora, fai clic qui per iniziare!") Questo perché stai restituendo una vista immutabile di una collezione mutabile , quindi il conteggio che ottieni ora potrebbe non essere uguale al conteggio tra tre microsecondi da ora. Quindi, mentre restituire una raccolta ti dà un modo "semplice" per arrivare al conteggio (e in realtà stream.count()è anche abbastanza facile), quel numero non è molto significativo per qualcosa di diverso dal debug o dalla stima.
Brian Goetz,

0

Mentre alcuni degli intervistati di più alto profilo hanno dato ottimi consigli generali, sono sorpreso che nessuno abbia mai affermato:

Se hai già un "materializzato" Collectionin mano (cioè è già stato creato prima della chiamata - come nel caso dell'esempio dato, dove è un campo membro), non ha senso convertirlo in a Stream. Il chiamante può farlo facilmente da solo. Considerando che, se il chiamante vuole consumare i dati nella sua forma originale, convertirli in un Streamli costringe a fare un lavoro ridondante per materializzare nuovamente una copia della struttura originale.


-1

Forse una fabbrica Stream sarebbe una scelta migliore. La grande vittoria di esporre solo raccolte tramite Stream è che incapsula meglio la struttura dei dati del tuo modello di dominio. È impossibile per qualsiasi uso delle tue classi di dominio influenzare il funzionamento interno del tuo Elenco o Set semplicemente esponendo uno Stream.

Incoraggia anche gli utenti della tua classe di dominio a scrivere codice in uno stile Java 8 più moderno. È possibile eseguire il refactoring incrementale di questo stile mantenendo i getter esistenti e aggiungendo nuovi getter che restituiscono stream. Nel tempo, puoi riscrivere il codice legacy fino a quando non hai eliminato definitivamente tutti i getter che restituiscono un elenco o un set. Questo tipo di refactoring si sente davvero bene dopo aver cancellato tutto il codice legacy!


7
c'è un motivo per cui questo è completamente citato? c'è una fonte?
Xerus,

-5

Probabilmente avrei 2 metodi, uno per restituire a Collectione uno per restituire la raccolta come a Stream.

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

// ...

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

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

}

Questo è il migliore dei due mondi. Il client può scegliere se desidera l'elenco o lo stream e non deve eseguire la creazione di oggetti extra per creare una copia immutabile dell'elenco solo per ottenere uno stream.

Questo aggiunge anche solo un altro metodo alla tua API, quindi non hai troppi metodi


1
Perché voleva scegliere tra queste due opzioni e ha chiesto i pro ei contro di ognuna. Inoltre fornisce a tutti una migliore comprensione di questi concetti.
Libert Piou Piou,

Per favore, non farlo. Immagina le API!
François Gautier,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.