Modifica di oggetti all'interno del flusso in Java8 durante l'iterazione


88

Negli stream Java8, posso modificare / aggiornare gli oggetti all'interno? Per es. List<User> users:

users.stream().forEach(u -> u.setProperty("value"))

Risposte:


102

Sì, puoi modificare lo stato degli oggetti all'interno del tuo flusso, ma molto spesso dovresti evitare di modificare lo stato dell'origine del flusso. Dalla sezione sulla non interferenza della documentazione del pacchetto stream possiamo leggere che:

Per la maggior parte delle origini dati, prevenire le interferenze significa garantire che l'origine dati non venga modificata durante l'esecuzione della pipeline di flusso. La notevole eccezione a ciò sono i flussi le cui origini sono raccolte simultanee, progettate specificamente per gestire la modifica simultanea. Le sorgenti di flusso simultanee sono quelle il cui Spliteratorrapporto sulla CONCURRENTcaratteristica.

Quindi va bene

  List<User> users = getUsers();
  users.stream().forEach(u -> u.setProperty(value));
//                       ^    ^^^^^^^^^^^^^

ma questo nella maggior parte dei casi non lo è

  users.stream().forEach(u -> users.remove(u));
//^^^^^                       ^^^^^^^^^^^^

e può generare ConcurrentModificationExceptiono anche altre eccezioni impreviste come NPE:

List<Integer> list = IntStream.range(0, 10).boxed().collect(Collectors.toList());

list.stream()
    .filter(i -> i > 5)
    .forEach(i -> list.remove(i));  //throws NullPointerException

4
Quali sono le soluzioni se vuoi modificare gli utenti?
Augustas

2
@ Augustas Dipende tutto da come vuoi modificare questo elenco. Ma generalmente dovresti evitare Iteratorche impedisce la modifica dell'elenco (rimozione / aggiunta di nuovi elementi) durante l'iterazione e sia i flussi che i cicli for-each lo stanno usando. Potresti provare a usare un ciclo semplice come il for(int i=0; ..; ..)quale non ha questo problema (ma non ti fermerà quando altri thread modificheranno la tua lista). Si potrebbe anche usare metodi come list.removeAll(Collection), list.removeIf(Predicate). Inoltre la java.util.Collectionsclasse ha pochi metodi che potrebbero essere utili come addAll(CollectionOfNewElements,list).
Pshemo

@Pshemo, una soluzione è creare una nuova istanza di una raccolta come ArrayList con gli elementi all'interno della tua lista principale; itera sulla nuova lista ed esegui l'operazione sulla lista principale: new ArrayList <> (users) .stream.forEach (u -> users.remove (u));
MNZ

2
@Blauhirn Risolvere cosa? Non è consentito modificare l'origine del flusso durante l'utilizzo di flusso, ma è consentito modificare lo stato dei suoi elementi. Non importa se usiamo map, foreach o altri metodi di streaming.
Pshemo

1
Invece di modificare la raccolta, suggerirei di utilizzarefilter()
jontro

5

Il modo funzionale sarebbe:

import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class PredicateTestRun {

    public static void main(String[] args) {

        List<String> lines = Arrays.asList("a", "b", "c");
        System.out.println(lines); // [a, b, c]
        Predicate<? super String> predicate = value -> "b".equals(value);
        lines = lines.stream().filter(predicate.negate()).collect(toList());

        System.out.println(lines); // [a, c]
    }
}

In questa soluzione la lista originale non viene modificata, ma dovrebbe contenere il risultato atteso in una nuova lista accessibile sotto la stessa variabile di quella vecchia


3

Per apportare modifiche strutturali alla sorgente del flusso, come ha menzionato Pshemo nella sua risposta, una soluzione è creare una nuova istanza di un Collectionsimile ArrayListcon gli elementi all'interno della tua lista principale; iterare sul nuovo elenco ed eseguire le operazioni sull'elenco principale.

new ArrayList<>(users).stream().forEach(u -> users.remove(u));

1

Per sbarazzarsi di ConcurrentModificationException utilizzare CopyOnWriteArrayList


2
Questo è effettivamente corretto, ma: questo dipende dalla classe specifica che viene utilizzata qui. Ma quando sai solo di avere un List<Something>- non hai idea se si tratta di un CopyOnWriteArrayList. Quindi questo più simile a un commento mediocre, ma non una vera risposta.
GhostCat

Se lo avesse pubblicato come commento, molto probabilmente qualcuno gli avrebbe detto che non avrebbe dovuto pubblicare risposte come commenti.
Bill K

1

Invece di creare cose strane, puoi solo filter()e poi il map()tuo risultato.

Questo è molto più leggibile e sicuro. Gli stream lo faranno in un solo loop.


1

Puoi utilizzare il file removeIf per rimuovere i dati da un elenco in modo condizionale.

Ad esempio: - Se vuoi rimuovere tutti i numeri pari da un elenco, puoi farlo come segue.

    final List<Integer> list = IntStream.range(1,100).boxed().collect(Collectors.toList());

    list.removeIf(number -> number % 2 == 0);

1

Sì, puoi modificare o aggiornare i valori degli oggetti nell'elenco nel tuo caso allo stesso modo:

users.stream().forEach(u -> u.setProperty("some_value"))

Tuttavia, la dichiarazione precedente apporterà aggiornamenti sugli oggetti di origine. Che potrebbe non essere accettabile nella maggior parte dei casi.

Fortunatamente, abbiamo un altro modo come:

List<Users> updatedUsers = users.stream().map(u -> u.setProperty("some_value")).collect(Collectors.toList());

Che restituisce un elenco aggiornato, senza ostacolare quello vecchio.


0

Come accennato in precedenza, non è possibile modificare l'elenco originale, ma è possibile eseguire lo streaming, modificare e raccogliere elementi in un nuovo elenco. Ecco un semplice esempio di come modificare l'elemento stringa.

public class StreamTest {

    @Test
    public void replaceInsideStream()  {
        List<String> list = Arrays.asList("test1", "test2_attr", "test3");
        List<String> output = list.stream().map(value -> value.replace("_attr", "")).collect(Collectors.toList());
        System.out.println("Output: " + output); // Output: [test1, test2, test3]
    }
}

-1

Potrebbe essere un po 'tardi. Ma qui è uno degli usi. Questo per ottenere il conteggio del numero di file.

Crea un puntatore alla memoria (un nuovo oggetto in questo caso) e modifica la proprietà dell'oggetto. Il flusso Java 8 non consente di modificare il puntatore stesso e quindi se dichiari conta solo come variabile e provi ad aumentare all'interno del flusso, non funzionerà mai e genererà un'eccezione del compilatore in primo luogo

Path path = Paths.get("/Users/XXXX/static/test.txt");



Count c = new Count();
            c.setCount(0);
            Files.lines(path).forEach(item -> {
                c.setCount(c.getCount()+1);
                System.out.println(item);});
            System.out.println("line count,"+c);

public static class Count{
        private int count;

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }

        @Override
        public String toString() {
            return "Count [count=" + count + "]";
        }



    }
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.