Come aggiungere elementi di un flusso Java8 in un elenco esistente


Risposte:


198

NOTA: la risposta di nosid mostra come aggiungere a una raccolta esistente usando forEachOrdered(). Questa è una tecnica utile ed efficace per la mutazione delle raccolte esistenti. La mia risposta spiega perché non dovresti usare a Collectorper mutare una collezione esistente.

La risposta breve è no , almeno non in generale, non dovresti usare a Collectorper modificare una collezione esistente.

Il motivo è che i collezionisti sono progettati per supportare il parallelismo, anche su raccolte che non sono thread-safe. Il modo in cui lo fanno è far sì che ogni thread operi in modo indipendente sulla propria raccolta di risultati intermedi. Il modo in cui ogni thread ottiene la propria raccolta è di chiamare quello Collector.supplier()che è necessario per restituire una nuova raccolta ogni volta.

Queste raccolte di risultati intermedi vengono quindi unite, sempre in modo limitato, fino a quando non si ottiene un'unica raccolta di risultati. Questo è il risultato finale collect()dell'operazione.

Un paio di risposte di Balder e Assylias hanno suggerito di utilizzare Collectors.toCollection()e quindi passare un fornitore che restituisce un elenco esistente anziché un nuovo elenco. Ciò viola il requisito per il fornitore, che è quello di restituire ogni volta una nuova raccolta vuota.

Questo funzionerà per casi semplici, come dimostrano gli esempi nelle loro risposte. Tuttavia, fallirà, in particolare se il flusso viene eseguito in parallelo. (Una versione futura della libreria potrebbe cambiare in un modo imprevisto che potrebbe causare un errore, anche nel caso sequenziale.)

Facciamo un semplice esempio:

List<String> destList = new ArrayList<>(Arrays.asList("foo"));
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");
newList.parallelStream()
       .collect(Collectors.toCollection(() -> destList));
System.out.println(destList);

Quando eseguo questo programma, ottengo spesso un ArrayIndexOutOfBoundsException. Questo perché più thread stanno funzionando ArrayList, una struttura dati non sicura. OK, rendiamolo sincronizzato:

List<String> destList =
    Collections.synchronizedList(new ArrayList<>(Arrays.asList("foo")));

Questo non fallirà più con un'eccezione. Ma invece del risultato atteso:

[foo, 0, 1, 2, 3]

dà strani risultati come questo:

[foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0, foo, 2, 3, foo, 2, 3, 1, 0]

Questo è il risultato delle operazioni di accumulazione / fusione limitate dal thread che ho descritto sopra. Con un flusso parallelo, ogni thread chiama il fornitore per ottenere la propria raccolta per l'accumulo intermedio. Se si passa a un fornitore che restituisce la stessa raccolta, ogni thread aggiunge i risultati a quella raccolta. Poiché non esiste alcun ordinamento tra i thread, i risultati verranno aggiunti in un ordine arbitrario.

Quindi, quando queste raccolte intermedie vengono unite, fondamentalmente si fonde l'elenco con se stesso. Gli elenchi vengono uniti utilizzando List.addAll(), il che indica che i risultati non sono definiti se la raccolta di origine viene modificata durante l'operazione. In questo caso, ArrayList.addAll()esegue un'operazione di copia dell'array, quindi finisce per duplicarsi, il che è una specie di quello che ci si aspetterebbe, immagino. (Si noti che altre implementazioni di Elenco potrebbero avere un comportamento completamente diverso.) Comunque, questo spiega i risultati strani e gli elementi duplicati nella destinazione.

Potresti dire: "Mi accerterò solo di eseguire il mio stream in sequenza" e andare avanti e scrivere codice in questo modo

stream.collect(Collectors.toCollection(() -> existingList))

Comunque. Lo sconsiglio di farlo. Se controlli lo stream, certo, puoi garantire che non funzionerà in parallelo. Mi aspetto che emergerà uno stile di programmazione in cui i flussi vengono distribuiti anziché le raccolte. Se qualcuno ti passa uno stream e usi questo codice, fallirà se lo stream sembra essere parallelo. Peggio ancora, qualcuno potrebbe offrirti un flusso sequenziale e questo codice funzionerà bene per un po ', supererà tutti i test, ecc. Quindi, un po' di tempo arbitrario in seguito, il codice in altre parti del sistema potrebbe cambiare per usare flussi paralleli che causeranno il tuo codice rompere.

OK, quindi assicurati di ricordare di chiamare sequential()su qualsiasi stream prima di utilizzare questo codice:

stream.sequential().collect(Collectors.toCollection(() -> existingList))

Certo, ti ricorderai di farlo ogni volta, giusto? :-) Diciamo che lo fai. Quindi, il team delle prestazioni si chiederà perché tutte le loro implementazioni parallele accuratamente realizzate non stiano fornendo alcuna accelerazione. E ancora una volta lo rintracceranno nel tuo codice che sta forzando l'esecuzione in sequenza dell'intero flusso.

Non farlo


Grande spiegazione! - grazie per aver chiarito questo. Modificherò la mia risposta per raccomandare di non farlo mai con possibili flussi paralleli.
Balder

3
Se la domanda è: se esiste una riga singola per aggiungere elementi di un flusso in un elenco esistente, la risposta breve è . Vedi la mia risposta Tuttavia, sono d'accordo con te sul fatto che l'utilizzo di Collectors.toCollection () in combinazione con un elenco esistente sia sbagliato.
nosid

Vero. Immagino che tutti noi pensassimo ai collezionisti.
Stuart segna il

Bella risposta! Sono molto tentato di usare la soluzione sequenziale anche se sconsigli chiaramente perché, come detto, deve funzionare bene. Ma il fatto che javadoc richieda che l'argomento fornitore del toCollectionmetodo debba restituire una raccolta nuova e vuota ogni volta mi convince a non farlo. Voglio davvero rompere il contratto javadoc delle classi Java di base.
zoom

1
@AlexCurvers Se vuoi che lo stream abbia effetti collaterali, quasi sicuramente lo vuoi usare forEachOrdered. Gli effetti collaterali includono l'aggiunta di elementi a una raccolta esistente, indipendentemente dal fatto che abbia già elementi. Se desideri che gli elementi di uno stream vengano inseriti in una nuova raccolta, usa collect(Collectors.toList())o toSet()o toCollection().
Stuart segna il

169

Per quanto posso vedere, tutte le altre risposte finora hanno utilizzato un raccoglitore per aggiungere elementi a un flusso esistente. Tuttavia, esiste una soluzione più breve e funziona sia per flussi sequenziali che paralleli. Puoi semplicemente usare il metodo per ogni ordine in combinazione con un riferimento al metodo.

List<String> source = ...;
List<Integer> target = ...;

source.stream()
      .map(String::length)
      .forEachOrdered(target::add);

L'unica limitazione è che l' origine e la destinazione sono elenchi diversi, poiché non è consentito apportare modifiche all'origine di uno stream purché sia ​​elaborato.

Si noti che questa soluzione funziona sia per flussi sequenziali che paralleli. Tuttavia, non beneficia della concorrenza. Il riferimento al metodo passato a forEachOrdered verrà sempre eseguito in sequenza.


6
+1 È divertente il numero di persone che affermano che non esiste alcuna possibilità quando ce n'è una. Btw. Ho incluso forEach(existing::add)come possibilità in una risposta due mesi fa . Avrei dovuto aggiungere forEachOrderedanche ...
Holger

5
C'è qualche motivo che hai usato al forEachOrderedposto di forEach?
membri del

6
@membersound: forEachOrderedfunziona con flussi sia sequenziali che paralleli . Al contrario, forEachpotrebbe eseguire contemporaneamente l'oggetto funzione passato per flussi paralleli. In questo caso, l'oggetto funzione deve essere sincronizzato correttamente, ad es. Utilizzando a Vector<Integer>.
nosid

@BrianGoetz: Devo ammettere che la documentazione di Stream.forEachOrdered è un po 'imprecisa. Tuttavia, non riesco a vedere qualsiasi ragionevole interpretazione di questo specifica , in cui non v'è no accade-prima relazione tra due richiami di target::add. Indipendentemente da quali thread viene invocato il metodo, non esiste alcuna gara di dati . Mi sarei aspettato che tu lo sapessi.
nosid,

Questa è la risposta più utile, per quanto mi riguarda. In realtà mostra un modo pratico per inserire elementi in un elenco esistente da uno stream, che è ciò che la domanda ha posto (nonostante la parola fuorviante "raccogliere")
Wheezil,

12

La risposta breve è no (o dovrebbe essere no). EDIT: sì, è possibile (vedi la risposta di assylias sotto), ma continua a leggere. EDIT2: ma vedi la risposta di Stuart Marks per l'ennesimo motivo per cui non dovresti ancora farlo!

La risposta più lunga:

Lo scopo di questi costrutti in Java 8 è quello di introdurre alcuni concetti di programmazione funzionale ; nella Programmazione Funzionale, le strutture di dati non vengono in genere modificate, ma ne vengono create di nuove a partire da vecchie per mezzo di trasformazioni come mappa, filtro, piega / riduci e molte altre.

Se devi modificare il vecchio elenco, raccogli semplicemente gli elementi mappati in un nuovo elenco:

final List<Integer> newList = list.stream()
                                  .filter(n -> n % 2 == 0)
                                  .collect(Collectors.toList());

e poi fallo list.addAll(newList)ancora: se proprio devi.

(o costruisci un nuovo elenco concatenando quello vecchio e quello nuovo, e assegnalo alla listvariabile — questo è un po ' più nello spirito di FP che addAll)

Per quanto riguarda l'API: anche se l'API lo consente (di nuovo, vedi la risposta di Assylias) dovresti cercare di evitare di farlo indipendentemente, almeno in generale. È meglio non combattere il paradigma (FP) e cercare di impararlo piuttosto che combatterlo (anche se Java generalmente non è un linguaggio FP), e ricorrere a tattiche "più sporche" solo se assolutamente necessario.

La risposta davvero lunga: (vale a dire se si include lo sforzo di trovare e leggere un'intro / libro FP come suggerito)

Scoprire perché modificare gli elenchi esistenti è in genere una cattiva idea e porta a un codice meno gestibile, a meno che non si modifichi una variabile locale e l'algoritmo sia breve e / o banale, che non rientra nell'ambito della questione della manutenibilità del codice - trova una buona introduzione alla programmazione funzionale (ce ne sono centinaia) e inizia a leggere. Una spiegazione di "anteprima" sarebbe qualcosa di simile: è matematicamente più sana e più facile ragionare sul fatto di non modificare i dati (nella maggior parte delle parti del programma) e porta a un livello superiore e meno tecnico (oltre che più umano, una volta che il tuo cervello transizioni dal vecchio pensiero imperativo) definizioni della logica del programma.


@assylias: logicamente, non era sbagliato perché c'era o parte; comunque, ha aggiunto una nota.
Erik Kaplun,

1
La risposta breve è giusta. Le linee guida proposte avranno successo in casi semplici ma falliranno nel caso generale.
Stuart segna il

La risposta più lunga è per lo più corretta, ma la progettazione dell'API riguarda principalmente il parallelismo e meno sulla programmazione funzionale. Anche se ovviamente ci sono molte cose su FP che sono suscettibili di parallelismo, quindi questi due concetti sono ben allineati.
Stuart segna il

@StuartMarks: Interessante: in quali casi si rompe la soluzione fornita nella risposta di Assylias? (e aspetti positivi del parallelismo: immagino di essere troppo ansioso di difendere FP)
Erik Kaplun

@ErikAllik Ho aggiunto una risposta che tratta questo problema.
Stuart segna il

11

Erik Allik ha già fornito ottime ragioni, perché molto probabilmente non vorrai raccogliere elementi di un flusso in un Elenco esistente.

Ad ogni modo, puoi usare il seguente one-liner, se hai davvero bisogno di questa funzionalità.

Ma come spiega Stuart Marks nella sua risposta, non dovresti mai farlo, se i flussi potrebbero essere flussi paralleli - usa a tuo rischio ...

list.stream().collect(Collectors.toCollection(() -> myExistingList));

ahh, è un peccato: P
Erik Kaplun

2
Questa tecnica fallirà in modo orribile se il flusso viene eseguito in parallelo.
Stuart segna il

1
Sarebbe responsabilità del fornitore della raccolta assicurarsi che non fallisca, ad esempio fornendo una raccolta concorrente.
Balder

2
No, questo codice viola il requisito di toCollection (), che prevede che il fornitore restituisca una nuova raccolta vuota del tipo appropriato. Anche se la destinazione è thread-safe, l'unione effettuata per il caso parallelo genererà risultati errati.
Stuart segna il

1
@ Balder Ho aggiunto una risposta che dovrebbe chiarire questo.
Stuart segna il

4

Devi solo fare riferimento al tuo elenco originale per essere quello che Collectors.toList()restituisce.

Ecco una demo:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Reference {

  public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    System.out.println(list);

    // Just collect even numbers and start referring the new list as the original one.
    list = list.stream()
               .filter(n -> n % 2 == 0)
               .collect(Collectors.toList());
    System.out.println(list);
  }
}

Ed ecco come puoi aggiungere gli elementi appena creati al tuo elenco originale in una sola riga.

List<Integer> list = ...;
// add even numbers from the list to the list again.
list.addAll(list.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList())
);

Questo è ciò che offre questo paradigma di programmazione funzionale.


Volevo dire come aggiungere / raccogliere in un elenco esistente non solo riassegnare.
codefx

1
Beh, tecnicamente non puoi fare questo tipo di cose nel paradigma di Programmazione Funzionale, di cui i flussi riguardano tutto. Nella programmazione funzionale, lo stato non viene modificato, invece, vengono creati nuovi stati in strutture di dati persistenti, rendendolo sicuro per scopi di concorrenza e più funzionale. L'approccio che ho citato è ciò che puoi fare, oppure puoi ricorrere all'approccio orientato agli oggetti vecchio stile in cui esegui l'iterazione su ciascun elemento e mantieni o rimuovi gli elementi come ritieni opportuno.
Aman Agnihotri,

0

targetList = sourceList.stream (). flatmap (List :: stream) .collect (Collectors.toList ());


0

Concatenerei la vecchia lista e la nuova lista come flussi e salverei i risultati nella lista destinazioni. Funziona bene anche in parallelo.

Userò l'esempio di risposta accettata dato da Stuart Marks:

List<String> destList = Arrays.asList("foo");
List<String> newList = Arrays.asList("0", "1", "2", "3", "4", "5");

destList = Stream.concat(destList.stream(), newList.stream()).parallel()
            .collect(Collectors.toList());
System.out.println(destList);

//output: [foo, 0, 1, 2, 3, 4, 5]

Spero che sia d'aiuto.

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.