it-swarm.com.de

Ändern der lokalen Variablen von innen nach Lambda

Das Ändern einer lokalen Variablen in forEach fü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ösen kann?

46
Patan

Verwenden Sie einen Wrapper

Mit Java 8+ :

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

Mit Java 10+ :

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

Wenn Sie den Wert nur von außen in das Lambda eingeben und nicht herausholen müssen, können Sie dies mit einer normalen anonymen Klasse anstelle eines Lambda tun:

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

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

Nach meiner Erfahrung stellt sich in mehr als 80% der Fälle die Frage, wie man ein erobertes Lokales aus einem Lambda mutiert. Normalerweise beinhaltet dies eine Reduzierung, aber in diesem Fall ist die Technik, einen Stream über die Listenindizes auszuführen, gut anwendbar:

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

Da die verwendeten Variablen außerhalb der Lamda (implizit) final sein müssen, müssen Sie etwas wie AtomicInteger verwenden oder Ihre eigene Datenstruktur schreiben.

Siehe https://docs.Oracle.com/javase/tutorial/Java/javaOO/lambdaexpressions.html#accessing-local-variables .

7
flo

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 sehen Sie die Antwort von Stuart : Es könnte einen besseren Weg geben, mit Ihrem Fall umzugehen.

3
zakmck

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

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

Vielleicht nicht das eleganteste oder sogar das richtigste, aber es wird es tun. 

2
Almir Campos

Wenn Sie Java 10 verwenden, können Sie var dafür verwenden:

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

Sie können es abarbeiten, um den Compiler zu umgehen, aber denken Sie daran, dass Nebenwirkungen in Lambdas nicht empfohlen werden. 

Zitieren der javadoc

Nebenwirkungen in Verhaltensparametern für Streaming-Vorgänge werden im Allgemeinen nicht empfohlen, da sie häufig zu ungewollten Verstößen gegen die Anforderung an die Staatenlosigkeit führen können Eine kleine Anzahl von Stream-Operationen wie forEach () und peek () kann nur über Nebeneffekte ausgeführt werden. diese sollten mit Vorsicht verwendet werden

0
user2153465

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

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

Domain-Objekt:

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

Operation wiederholen:

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" als endgültig festlegen, aber ich habe keine Kompilierungsfehler erhalten, die diese Details so belassen.

0
Steve T

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 Lösung von Almir Campos).

Im konkreten Fall ist dies keine gute Lösung, da Integer für Ihren Zweck schlechter ist als int, trotzdem ist diese Lösung meiner Meinung nach allgemeiner.

0
luca.vercelli