Ändern der lokalen Variablen innerhalb von Lambda


114

Das Ändern einer lokalen Variablen in forEachführt zu einem Kompilierungsfehler:

Normal

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

Mit Lambda

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

Irgendeine Idee, wie man das löst?


7
Angesichts der Tatsache, dass Lambdas im Wesentlichen syntaktischer Zucker für eine anonyme innere Klasse sind, ist meine Intuition, dass es unmöglich ist, eine nicht endgültige lokale Variable zu erfassen. Ich würde mich gerne als falsch erweisen.
Sinkingpoint

2
Eine in einem Lambda-Ausdruck verwendete Variable muss effektiv endgültig sein. Sie könnten eine atomare Ganzzahl verwenden, obwohl sie übertrieben ist, sodass ein Lambda-Ausdruck hier nicht wirklich benötigt wird. Bleib einfach bei der for-Schleife.
Alexis C.

3
Die Variable muss effektiv endgültig sein . Siehe dies: Warum die Einschränkung der Erfassung lokaler Variablen?
Jesper


2
@Quirliom Sie sind kein syntaktischer Zucker für anonyme Klassen. Lambdas verwenden Methodengriffe unter der Haube
Dioxin

Antworten:


175

Verwenden Sie eine Hülle

Jede Art von Verpackung ist gut.

Mit Java 8+ , verwenden entweder ein AtomicInteger:

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

... oder ein Array:

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

Mit Java 10+ :

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

Hinweis: Seien Sie sehr vorsichtig, wenn Sie einen parallelen Stream verwenden. Möglicherweise erhalten Sie nicht das erwartete Ergebnis. Andere Lösungen wie die von Stuart sind möglicherweise besser für diese Fälle geeignet.

Für andere Typen als int

Dies gilt natürlich weiterhin für andere Typen als int. Sie müssen nur den Umhüllungstyp in ein AtomicReferenceoder ein Array dieses Typs ändern . Wenn Sie beispielsweise a verwenden String, gehen Sie wie folgt vor:

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

Verwenden Sie ein Array:

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

Oder mit Java 10+:

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

Warum darfst du ein Array wie verwenden int[] ordinal = { 0 };? Kannst du es erklären. Vielen Dank
Mrbela

@mrbela Was genau verstehst du nicht? Vielleicht kann ich das genau klären?
Olivier Grégoire

1
@ Mrbela-Arrays werden als Referenz übergeben. Wenn Sie ein Array übergeben, übergeben Sie tatsächlich eine Speicheradresse für dieses Array. Primitive Typen wie Integer werden nach Wert gesendet, was bedeutet, dass eine Kopie des Werts übergeben wird. Bei der Übergabe nach Wert besteht keine Beziehung zwischen dem ursprünglichen Wert und der in Ihre Methoden gesendeten Kopie. Durch Manipulieren der Kopie wird nichts mit dem Original geändert. Wenn Sie als Referenz übergeben, bedeutet dies, dass der ursprüngliche Wert und der an die Methode gesendete Wert identisch sind. Wenn Sie ihn in Ihrer Methode bearbeiten, wird der Wert außerhalb der Methode geändert.
DetectivePikachu

@Olivier Grégoire das hat meine Haut wirklich gerettet. Ich habe die Java 10-Lösung mit "var" verwendet. Können Sie erklären, wie das funktioniert? Ich habe überall gegoogelt und dies ist der einzige Ort, den ich mit dieser speziellen Verwendung gefunden habe. Ich vermute, da ein Lambda nur (effektiv) endgültige Objekte von außerhalb seines Gültigkeitsbereichs zulässt, täuscht das "var" -Objekt den Compiler grundsätzlich zu der Schlussfolgerung, dass es endgültig ist, da davon ausgegangen wird, dass nur endgültige Objekte außerhalb des Gültigkeitsbereichs referenziert werden. Ich weiß nicht, das ist meine beste Vermutung. Wenn Sie eine Erklärung
abgeben möchten,

1
@oaker Dies funktioniert, weil Java die Klasse MyClass $ 1 wie folgt erstellt: class MyClass$1 { int value; }In einer normalen Klasse bedeutet dies, dass Sie eine Klasse mit einer paketprivaten Variablen namens erstellen value. Dies ist das gleiche, aber die Klasse ist für uns tatsächlich anonym, nicht für den Compiler oder die JVM. Java kompiliert den Code so, als ob er es wäre MyClass$1 wrapper = new MyClass$1();. Und die anonyme Klasse wird nur eine weitere Klasse. Am Ende haben wir nur syntaktischen Zucker hinzugefügt, um ihn lesbar zu machen. Außerdem ist die Klasse eine innere Klasse mit einem paketprivaten Feld. Dieses Feld ist verwendbar.
Olivier Grégoire

14

Dies kommt einem XY-Problem ziemlich nahe . Das heißt, die Frage, die gestellt wird, ist im Wesentlichen, wie eine erfasste lokale Variable von einem Lambda mutiert werden kann. Die eigentliche Aufgabe besteht jedoch darin, die Elemente einer Liste zu nummerieren.

Nach meiner Erfahrung gibt es in mehr als 80% der Fälle die Frage, wie ein gefangener Einheimischer aus einem Lambda heraus mutiert werden kann. Es gibt einen besseren Weg, um fortzufahren. Normalerweise beinhaltet dies eine Reduzierung, aber in diesem Fall gilt die Technik, einen Stream über die Listenindizes auszuführen, gut:

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

3
Gute Lösung, aber nur, wenn listes eine RandomAccessListe gibt
ZhekaKozlov

Ich wäre gespannt, ob das Problem der Fortschrittsmeldung alle k Iterationen praktisch ohne einen dedizierten Zähler gelöst werden kann, dh in diesem Fall: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("Ich habe {} Elemente verarbeitet", ctr);} Ich sehe keinen praktischeren Weg (Reduktion könnte dies tun, wäre aber ausführlicher, insbesondere bei parallelen Streams).
zakmck

14

Wenn Sie den Wert nur von außen an das Lambda übergeben müssen und ihn nicht herausholen müssen, können Sie dies mit einer regulären anonymen Klasse anstelle eines Lambda tun:

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

1
... und was ist, wenn Sie das Ergebnis tatsächlich lesen müssen? Das Ergebnis ist für Code auf der obersten Ebene nicht sichtbar.
Luke Usherwood

@ LukeUsherwood: Du hast recht. Dies gilt nur, wenn Sie nur Daten von außen an das Lambda übergeben müssen, nicht aber herausholen müssen. Wenn Sie es herausholen möchten, muss das Lambda einen Verweis auf ein veränderliches Objekt, z. B. ein Array oder ein Objekt mit nicht endgültigen öffentlichen Feldern, erfassen und Daten übergeben, indem Sie es in das Objekt setzen.
Newacct


3

Wenn Sie mit Java 10 arbeiten, können Sie Folgendes verwenden var:

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

3

Eine Alternative zu AtomicInteger(oder einem anderen Objekt, das einen Wert speichern kann) ist die Verwendung eines Arrays:

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

Aber siehe die Antwort von Stuart : Es könnte einen besseren Weg geben, um mit Ihrem Fall umzugehen.


2

Ich weiß, dass dies eine alte Frage ist, aber wenn Sie mit einer Problemumgehung vertraut sind und die externe Variable endgültig sein sollte, können Sie dies einfach tun:

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

Vielleicht nicht das eleganteste oder sogar das korrekteste, aber es wird reichen.


1

Sie können es einpacken, um den Compiler zu umgehen. Beachten Sie jedoch, dass von Nebenwirkungen bei Lambdas abgeraten wird.

Um den Javadoc zu zitieren

Nebenwirkungen von Verhaltensparametern bei Stream-Vorgängen werden im Allgemeinen nicht empfohlen, da sie häufig zu unbeabsichtigten Verstößen gegen die Anforderung der Staatenlosigkeit führen können. Eine kleine Anzahl von Stream-Vorgängen wie forEach () und peek () kann nur über die Seite ausgeführt werden -Auswirkungen; Diese sollten mit Vorsicht verwendet werden


0

Ich hatte ein etwas anderes Problem. Anstatt eine lokale Variable in forEach zu erhöhen, musste ich der lokalen Variablen ein Objekt zuweisen.

Ich habe dieses Problem gelöst, indem ich eine private innere Domänenklasse definiert habe, die sowohl die Liste, über die ich iterieren möchte (countryList), als auch die Ausgabe, die ich von dieser Liste erhalten möchte (foundCountry), umschließt. Dann iteriere ich mit Java 8 "forEach" über das Listenfeld, und wenn das gewünschte Objekt gefunden wird, ordne ich dieses Objekt dem Ausgabefeld zu. Dadurch wird einem Feld der lokalen Variablen ein Wert zugewiesen, ohne dass die lokale Variable selbst geändert wird. Ich glaube, da sich die lokale Variable selbst nicht ändert, beschwert sich der Compiler nicht. Ich kann dann den Wert verwenden, den ich im Ausgabefeld außerhalb der Liste erfasst habe.

Domänenobjekt:

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

Wrapper-Objekt:

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

Iterierte Operation:

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());

Sie könnten die Wrapper-Klassenmethode "setCountryList ()" entfernen und das Feld "countryList" endgültig machen, aber ich habe keine Kompilierungsfehler erhalten, wenn diese Details unverändert bleiben.


0

Um eine allgemeinere Lösung zu erhalten, können Sie eine generische Wrapper-Klasse schreiben:

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++;
});

(Dies ist eine Variante der von Almir Campos gegebenen Lösung).

Im konkreten Fall ist dies keine gute Lösung, da sie Integerschlechter ist als intfür Ihren Zweck. Trotzdem denke ich, dass diese Lösung allgemeiner ist.


0

Ja, Sie können lokale Variablen in Lambdas ändern (wie in den anderen Antworten gezeigt), aber Sie sollten dies nicht tun. Lambdas sind für den funktionalen Programmierstil gedacht und das bedeutet: Keine Nebenwirkungen. Was Sie tun möchten, wird als schlechter Stil angesehen. Es ist auch gefährlich bei parallelen Streams.

Sie sollten entweder eine Lösung ohne Nebenwirkungen finden oder eine herkömmliche for-Schleife verwenden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.