Javadoc di Collector mostra come raccogliere elementi di un flusso in un nuovo elenco. Esiste un one-liner che aggiunge i risultati in una ArrayList esistente?
Javadoc di Collector mostra come raccogliere elementi di un flusso in un nuovo elenco. Esiste un one-liner che aggiunge i risultati in una ArrayList esistente?
Risposte:
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 Collector
per mutare una collezione esistente.
La risposta breve è no , almeno non in generale, non dovresti usare a Collector
per 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
toCollection
metodo 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.
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()
.
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.
forEach(existing::add)
come possibilità in una risposta due mesi fa . Avrei dovuto aggiungere forEachOrdered
anche ...
forEachOrdered
posto di forEach
?
forEachOrdered
funziona con flussi sia sequenziali che paralleli . Al contrario, forEach
potrebbe 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>
.
target::add
. Indipendentemente da quali thread viene invocato il metodo, non esiste alcuna gara di dati . Mi sarei aspettato che tu lo sapessi.
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 list
variabile — 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.
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));
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.
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.
Collection
"