Modifica della variabile locale dall'interno di lambda


115

La modifica di una variabile locale in forEachdà un errore di compilazione:

Normale

    int ordinal = 0;
    for (Example s : list) {
        s.setOrdinal(ordinal);
        ordinal++;
    }

Con Lambda

    int ordinal = 0;
    list.forEach(s -> {
        s.setOrdinal(ordinal);
        ordinal++;
    });

Qualche idea su come risolvere questo problema?


8
Considerando che i lambda sono essenzialmente zucchero sintattico per una classe interna anonima, la mia intuizione è che è impossibile catturare una variabile locale non finale. Mi piacerebbe però essere smentito.
Sinkingpoint

2
Una variabile utilizzata in un'espressione lambda deve essere effettivamente finale. È possibile utilizzare un numero intero atomico sebbene sia eccessivo, quindi un'espressione lambda non è realmente necessaria qui. Attenersi al ciclo for.
Alexis C.

3
La variabile deve essere effettivamente definitiva . Vedi questo: Perché la restrizione sull'acquisizione di variabili locali?
Jesper


2
@Quirliom Non sono zucchero sintattico per classi anonime. Lambdas usa metodi di gestione sotto il cofano
Dioxin

Risposte:


177

Usa un involucro

Qualsiasi tipo di involucro va bene.

Con Java 8+ , utilizza uno dei seguenti AtomicInteger:

AtomicInteger ordinal = new AtomicInteger(0);
list.forEach(s -> {
  s.setOrdinal(ordinal.getAndIncrement());
});

... o un array:

int[] ordinal = { 0 };
list.forEach(s -> {
  s.setOrdinal(ordinal[0]++);
});

Con Java 10+ :

var wrapper = new Object(){ int ordinal = 0; };
list.forEach(s -> {
  s.setOrdinal(wrapper.ordinal++);
});

Nota: fai molta attenzione se utilizzi un flusso parallelo. Potresti non ottenere il risultato atteso. Altre soluzioni come quella di Stuart potrebbero essere più adatte a questi casi.

Per tipi diversi da int

Naturalmente, questo è ancora valido per i tipi diversi da int. Hai solo bisogno di cambiare il tipo di wrapping in un AtomicReferenceo un array di quel tipo. Ad esempio, se utilizzi a String, procedi come segue:

AtomicReference<String> value = new AtomicReference<>();
list.forEach(s -> {
  value.set("blah");
});

Usa un array:

String[] value = { null };
list.forEach(s-> {
  value[0] = "blah";
});

O con Java 10+:

var wrapper = new Object(){ String value; }
list.forEach(s->{
  wrapper.value = "blah";
});

Perché ti è permesso usare un array come int[] ordinal = { 0 };? Puoi spiegarlo. Grazie
mrbela

@mrbela Cosa non capisci esattamente? Forse posso chiarire quel bit specifico?
Olivier Grégoire

1
Gli array @mrbela vengono passati per riferimento. Quando si passa in un array, si sta effettivamente passando un indirizzo di memoria per quell'array. I tipi primitivi come integer vengono inviati per valore, il che significa che viene passata una copia del valore. Pass-by-value non esiste alcuna relazione tra il valore originale e la copia inviata nei metodi: la manipolazione della copia non ha effetto sull'originale. Passare per riferimento significa che il valore originale e quello inviato nel metodo sono la stessa cosa: manipolarlo nel metodo cambierà il valore al di fuori del metodo.
DetectivePikachu

@Olivier Grégoire questo mi ha davvero salvato la pelle. Ho usato la soluzione Java 10 con "var". Puoi spiegare come funziona? Ho cercato su Google ovunque e questo è l'unico posto che ho trovato con questo particolare utilizzo. La mia ipotesi è che poiché un lambda consente solo (effettivamente) oggetti finali al di fuori del suo ambito, l'oggetto "var" fondamentalmente inganna il compilatore facendogli dedurre che è finale, poiché presume che solo gli oggetti finali saranno referenziati fuori dallo scope. Non lo so, è la mia ipotesi migliore. Se desideri fornire una spiegazione, lo apprezzerei, dal momento che mi piace sapere perché il mio codice funziona :)
oaker

1
@oaker Questo funziona perché Java creerà la classe MyClass $ 1 come segue: class MyClass$1 { int value; }In una normale classe, ciò significa che crei una classe con una variabile privata del pacchetto denominata value. Questo è lo stesso, ma la classe è effettivamente anonima per noi, non per il compilatore o la JVM. Java compilerà il codice come se lo fosse MyClass$1 wrapper = new MyClass$1();. E la classe anonima diventa solo un'altra classe. Alla fine, abbiamo aggiunto solo zucchero sintattico per renderlo leggibile. Inoltre, la classe è una classe interna con campo package-private. Quel campo è utilizzabile.
Olivier Grégoire,

14

Questo è abbastanza vicino a un problema XY . Cioè, la domanda che viene posta è essenzialmente come mutare una variabile locale catturata da un lambda. Ma il vero compito a portata di mano è come numerare gli elementi di un elenco.

Nella mia esperienza, fino all'80% delle volte c'è una domanda su come mutare un locale catturato dall'interno di un lambda, c'è un modo migliore per procedere. Di solito ciò comporta la riduzione, ma in questo caso si applica bene la tecnica di eseguire un flusso sugli indici della lista:

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));

3
Buona soluzione ma solo se listè un RandomAccesselenco
ZhekaKozlov

Sarei curioso di sapere se il problema del messaggio di progressione ogni k iterazioni può essere praticamente risolto senza un contatore dedicato, ovvero questo caso: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("Ho elaborato {} elementi", ctr);} Non vedo un modo più pratico (la riduzione potrebbe farlo, ma sarebbe più prolisso, specialmente con flussi paralleli).
zakmck

14

Se hai solo bisogno di passare il valore dall'esterno nel lambda e non farlo uscire, puoi farlo con una normale classe anonima invece di un lambda:

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});

1
... e se avessi bisogno di leggere effettivamente il risultato? Il risultato non è visibile al codice al livello superiore.
Luke Usherwood

@ LukeUsherwood: Hai ragione. Questo è solo per se hai solo bisogno di passare i dati dall'esterno nel lambda, non estrarli. Se è necessario estrarlo, è necessario che lambda acquisisca un riferimento a un oggetto modificabile, ad esempio un array o un oggetto con campi pubblici non finali, e passi i dati impostandoli nell'oggetto.
newacct


3

Se sei su Java 10, puoi usare varper quello:

var ordinal = new Object() { int value; };
list.forEach(s -> {
    s.setOrdinal(ordinal.value);
    ordinal.value++;
});

3

Un'alternativa a AtomicInteger(oa qualsiasi altro oggetto in grado di memorizzare un valore) è usare un array:

final int ordinal[] = new int[] { 0 };
list.forEach ( s -> s.setOrdinal ( ordinal[ 0 ]++ ) );

Ma guarda la risposta di Stuart : potrebbe esserci un modo migliore per affrontare il tuo caso.


2

So che è una vecchia domanda, ma se ti senti a tuo agio con una soluzione alternativa, considerando il fatto che la variabile esterna dovrebbe essere definitiva, puoi semplicemente farlo:

final int[] ordinal = new int[1];
list.forEach(s -> {
    s.setOrdinal(ordinal[0]);
    ordinal[0]++;
});

Forse non il più elegante, o anche il più corretto, ma andrà bene.


1

Puoi avvolgerlo per aggirare il compilatore, ma ricorda che gli effetti collaterali nei lambda sono scoraggiati.

Per citare il javadoc

Gli effetti collaterali nei parametri comportamentali per le operazioni di streaming sono, in generale, scoraggiati, poiché spesso possono portare a violazioni inconsapevoli del requisito di apolidia Un piccolo numero di operazioni di flusso, come forEach () e peek (), può operare solo tramite -Effetti; questi dovrebbero essere usati con attenzione


0

Ho avuto un problema leggermente diverso. Invece di incrementare una variabile locale in forEach, avevo bisogno di assegnare un oggetto alla variabile locale.

Ho risolto questo problema definendo una classe di dominio interno privato che racchiude sia l'elenco su cui desidero iterare (countryList) sia l'output che spero di ottenere da tale elenco (foundCountry). Quindi, utilizzando Java 8 "forEach", itero sul campo elenco e quando viene trovato l'oggetto che desidero, assegno quell'oggetto al campo di output. Quindi questo assegna un valore a un campo della variabile locale, senza modificare la variabile locale stessa. Credo che poiché la variabile locale stessa non viene modificata, il compilatore non si lamenta. Posso quindi utilizzare il valore che ho catturato nel campo di output, al di fuori dell'elenco.

Oggetto dominio:

public class Country {

    private int id;
    private String countryName;

    public Country(int id, String countryName){
        this.id = id;
        this.countryName = countryName;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCountryName() {
        return countryName;
    }

    public void setCountryName(String countryName) {
        this.countryName = countryName;
    }
}

Oggetto wrapper:

private class CountryFound{
    private final List<Country> countryList;
    private Country foundCountry;
    public CountryFound(List<Country> countryList, Country foundCountry){
        this.countryList = countryList;
        this.foundCountry = foundCountry;
    }
    public List<Country> getCountryList() {
        return countryList;
    }
    public void setCountryList(List<Country> countryList) {
        this.countryList = countryList;
    }
    public Country getFoundCountry() {
        return foundCountry;
    }
    public void setFoundCountry(Country foundCountry) {
        this.foundCountry = foundCountry;
    }
}

Operazione ripetuta:

int id = 5;
CountryFound countryFound = new CountryFound(countryList, null);
countryFound.getCountryList().forEach(c -> {
    if(c.getId() == id){
        countryFound.setFoundCountry(c);
    }
});
System.out.println("Country found: " + countryFound.getFoundCountry().getCountryName());

Potresti rimuovere il metodo della classe wrapper "setCountryList ()" e rendere finale il campo "countryList", ma non ho ricevuto errori di compilazione lasciando questi dettagli così come sono.


0

Per avere una soluzione più generale, puoi scrivere una classe Wrapper generica:

public static class Wrapper<T> {
    public T obj;
    public Wrapper(T obj) { this.obj = obj; }
}
...
Wrapper<Integer> w = new Wrapper<>(0);
this.forEach(s -> {
    s.setOrdinal(w.obj);
    w.obj++;
});

(questa è una variante della soluzione data da Almir Campos).

Nel caso specifico questa non è una buona soluzione, in quanto Integerè peggio che intper il tuo scopo, comunque questa soluzione credo sia più generale.


0

Sì, puoi modificare le variabili locali dall'interno dei lambda (nel modo mostrato dalle altre risposte), ma non dovresti farlo. I Lambda sono stati per uno stile di programmazione funzionale e questo significa: Nessun effetto collaterale. Quello che vuoi fare è considerato un cattivo stile. È anche pericoloso in caso di flussi paralleli.

Dovresti trovare una soluzione senza effetti collaterali o utilizzare un ciclo for tradizionale.

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.