Modyfikowanie zmiennej lokalnej z wnętrza lambdy


115

Modyfikacja zmiennej lokalnej w programie forEachpowoduje błąd kompilacji:

Normalna

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

Z Lambda

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

Masz jakiś pomysł, jak to rozwiązać?


8
Biorąc pod uwagę, że lambdy są zasadniczo cukrem syntaktycznym dla anonimowej klasy wewnętrznej, intuicja jest taka, że ​​niemożliwe jest uchwycenie zmiennej lokalnej, która nie jest ostateczna. Chciałbym jednak udowodnić, że się mylę.
Sinkingpoint

2
Zmienna używana w wyrażeniu lambda musi być efektywnie ostateczna. Możesz użyć atomowej liczby całkowitej, chociaż jest to przesada, więc wyrażenie lambda nie jest tutaj potrzebne. Po prostu trzymaj się pętli for.
Alexis C.

3
Zmienna musi być faktycznie ostateczna . Zobacz to: Dlaczego ograniczenie dotyczące przechwytywania zmiennych lokalnych?
Jesper

2
możliwy duplikat Dlaczego lambdy
MT0

2
@Quirliom Nie są one cukrem syntaktycznym dla zajęć anonimowych. Lambdy używają metody rączek pod maską
Dioxin

Odpowiedzi:


177

Użyj opakowania

Każdy rodzaj opakowania jest dobry.

W przypadku języka Java 8+ użyj AtomicInteger:

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

... lub tablica:

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

Z Javą 10+ :

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

Uwaga: zachowaj ostrożność, jeśli używasz strumienia równoległego. Możesz nie osiągnąć oczekiwanego rezultatu. Inne rozwiązania, takie jak Stuart, mogą być lepiej dostosowane do tych przypadków.

Dla typów innych niż int

Oczywiście jest to nadal ważne dla typów innych niż int. Musisz tylko zmienić typ zawijania na AtomicReferencetablicę lub tablicę tego typu. Na przykład, jeśli używasz a String, po prostu wykonaj następujące czynności:

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

Użyj tablicy:

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

Lub z Javą 10+:

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

Dlaczego możesz używać tablicy takiej jak int[] ordinal = { 0 };? Możesz to wyjaśnić. Dziękuję
mrbela

@mrbela Czego dokładnie nie rozumiesz? Może mógłbym wyjaśnić ten konkretny fragment?
Olivier Grégoire

1
Tablice @mrbela są przekazywane przez odwołanie. Kiedy przekazujesz tablicę, w rzeczywistości przekazujesz adres pamięci dla tej tablicy. Typy pierwotne, takie jak liczby całkowite, są wysyłane przez wartość, co oznacza, że ​​przekazywana jest kopia wartości. Przekazywana wartość nie ma związku między oryginalną wartością a kopią przesłaną do metod - manipulowanie kopią nie ma żadnego wpływu na oryginał. Przekazywanie przez referencję oznacza, że ​​oryginalna wartość i ta przesłana do metody są tym samym - manipulowanie nią w metodzie spowoduje zmianę wartości poza metodą.
DetectivePikachu,

@Olivier Grégoire to naprawdę uratowało moją skórę. Użyłem rozwiązania Java 10 z "var". Czy możesz wyjaśnić, jak to działa? Wszędzie szukałem go w Google i jest to jedyne miejsce, które znalazłem z tym konkretnym zastosowaniem. Domyślam się, że skoro lambda dopuszcza (efektywnie) tylko końcowe obiekty spoza swojego zakresu, obiekt "var" w zasadzie oszukuje kompilator, aby wywnioskował, że jest ostateczny, ponieważ zakłada, że ​​tylko końcowe obiekty będą odwoływać się poza zakresem. Nie wiem, to moje najlepsze przypuszczenie. Jeśli zechcesz podać wyjaśnienie, byłbym wdzięczny, ponieważ chcę wiedzieć, dlaczego mój kod działa :)
oaker

1
@oaker Działa to, ponieważ Java utworzy klasę MyClass $ 1 w następujący sposób: class MyClass$1 { int value; }W zwykłej klasie oznacza to, że tworzysz klasę z prywatną zmienną pakietu o nazwie value. To jest to samo, ale klasa jest w rzeczywistości anonimowa dla nas, a nie dla kompilatora lub JVM. Java skompiluje kod tak, jakby był MyClass$1 wrapper = new MyClass$1();. A klasa anonimowa staje się po prostu kolejną klasą. Na koniec dodaliśmy tylko cukier syntaktyczny, aby był czytelny. Ponadto klasa jest klasą wewnętrzną z polem pakiet-prywatny. To pole jest użyteczne.
Olivier Grégoire

14

Jest to dość bliskie problemowi XY . Oznacza to, że zasadniczo zadaje się pytanie, jak zmutować przechwyconą zmienną lokalną z lambda. Ale aktualnym zadaniem jest numerowanie elementów listy.

Z mojego doświadczenia wynika, że ​​w 80% przypadków w górę pojawia się pytanie, jak zmutować przechwycony lokalnie z wnętrza lambda, jest lepszy sposób postępowania. Zwykle wiąże się to z redukcją, ale w tym przypadku technika prowadzenia strumienia po indeksach list ma zastosowanie:

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

3
Dobre rozwiązanie, ale tylko jeśli listjest RandomAccesslista
ZhekaKozlov

Byłbym ciekawy, czy problem komunikatu o postępie każdej k iteracji można praktycznie rozwiązać bez dedykowanego licznika, czyli w tym przypadku: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("Przetworzyłem {} elementy", ctr);} Nie widzę bardziej praktycznego sposobu (redukcja mogłaby to zrobić, ale byłaby bardziej szczegółowa, szczególnie w przypadku równoległych strumieni).
zakmck

14

Jeśli potrzebujesz tylko przekazać wartość z zewnątrz do lambda, a nie ją wyciągać, możesz to zrobić za pomocą zwykłej klasy anonimowej zamiast lambdy:

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

1
... a co jeśli chcesz faktycznie odczytać wynik? Wynik nie jest widoczny dla kodu na najwyższym poziomie.
Luke Usherwood,

@LukeUsherwood: Masz rację. Dzieje się tak tylko wtedy, gdy potrzebujesz tylko przekazywać dane z zewnątrz do lambdy, a nie wyciągać ich. Jeśli chcesz to wydobyć, musisz sprawić, by lambda przechwyciła odniesienie do zmiennego obiektu, np. Tablicy, lub obiektu z nieostatecznymi polami publicznymi, i przekazała dane, ustawiając je w obiekcie.
newacct


3

Jeśli korzystasz z Java 10, możesz użyć vardo tego:

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

3

Alternatywą dla AtomicInteger(lub dowolnego innego obiektu, który może przechowywać wartość) jest użycie tablicy:

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

Ale spójrz na odpowiedź Stuarta : może być lepszy sposób na załatwienie sprawy.


2

Wiem, że to stare pytanie, ale jeśli nie masz nic przeciwko obejściu, biorąc pod uwagę fakt, że zmienna zewnętrzna powinna być ostateczna, możesz po prostu zrobić to:

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

Może nie jest to najbardziej eleganckie, a nawet najbardziej poprawne, ale wystarczy.


1

Możesz to zakończyć, aby obejść kompilator, ale pamiętaj, że efekty uboczne w lambdach są odradzane.

Cytując plik javadoc

Skutki uboczne parametrów behawioralnych operacji strumieniowych są generalnie odradzane, ponieważ często mogą prowadzić do nieświadomego naruszenia wymogu bezpaństwowości Niewielka liczba operacji na strumieniu, takich jak forEach () i peek (), może działać tylko z boku -efekty; należy ich używać ostrożnie


0

Miałem nieco inny problem. Zamiast zwiększać lokalną zmienną w forEach, musiałem przypisać obiekt do zmiennej lokalnej.

Rozwiązałem to, definiując prywatną klasę domeny wewnętrznej, która otacza zarówno listę, którą chcę iterować (countryList), jak i dane wyjściowe, które mam nadzieję uzyskać z tej listy (foundCountry). Następnie używając Java 8 „forEach”, iteruję po polu listy, a kiedy szukany obiekt zostanie znaleziony, przypisuję ten obiekt do pola wyjściowego. Więc to przypisuje wartość do pola zmiennej lokalnej, nie zmieniając samej zmiennej lokalnej. Uważam, że skoro sama zmienna lokalna nie jest zmieniana, kompilator nie narzeka. Następnie mogę użyć wartości, którą przechwyciłem w polu wyjściowym, poza listą.

Obiekt domeny:

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

Obiekt opakowania:

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

Operacja iteracyjna:

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

Możesz usunąć metodę klasy opakowania „setCountryList ()” i nadać polu „countryList” ostateczną wartość, ale nie otrzymałem błędów kompilacji, pozostawiając te szczegóły bez zmian.


0

Aby mieć bardziej ogólne rozwiązanie, możesz napisać ogólną klasę Wrapper:

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

(jest to wariant rozwiązania podany przez Almira Camposa).

W konkretnym przypadku nie jest to dobre rozwiązanie, ponieważ Integerjest gorsze niż intw twoim celu, zresztą to rozwiązanie jest bardziej ogólne.


0

Tak, możesz modyfikować zmienne lokalne z wnętrza lambd (w sposób pokazany przez inne odpowiedzi), ale nie powinieneś tego robić. Lambdy zostały stworzone dla funkcjonalnego stylu programowania, co oznacza: Brak efektów ubocznych. To, co chcesz robić, jest uważane za zły styl. Jest to również niebezpieczne w przypadku strumieni równoległych.

Powinieneś albo znaleźć rozwiązanie bez skutków ubocznych, albo użyć tradycyjnej pętli for.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.