Memodifikasi variabel lokal dari dalam lambda

115

Memodifikasi variabel lokal forEachmemberikan kesalahan kompilasi:

Normal

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

Dengan Lambda

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

Ada ide bagaimana mengatasi ini?

Patan
sumber
8
Mempertimbangkan lambda pada dasarnya adalah gula sintaksis untuk kelas dalam anonim, intuisi saya adalah tidak mungkin untuk menangkap variabel lokal non final. Saya ingin sekali terbukti salah.
Sinkingpoint
2
Variabel yang digunakan dalam ekspresi lambda harus final secara efektif. Anda dapat menggunakan integer atom meskipun berlebihan, jadi ekspresi lambda tidak terlalu diperlukan di sini. Tetap gunakan loop-for.
Alexis C.
3
Variabel harus final secara efektif . Lihat ini: Mengapa pembatasan penangkapan variabel lokal?
Jesper
2
@Quirliom Mereka bukan gula sintetik untuk kelas anonim. Metode penggunaan Lambdas menangani di bawah tenda
Dioxin

Jawaban:

177

Gunakan pembungkus

Semua jenis pembungkus itu bagus.

Dengan Java 8+ , gunakan salah satu AtomicInteger:

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

... atau array:

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

Dengan Java 10+ :

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

Catatan: berhati-hatilah jika Anda menggunakan aliran paralel. Anda mungkin tidak mendapatkan hasil yang diharapkan. Solusi lain seperti Stuart mungkin lebih disesuaikan untuk kasus tersebut.

Untuk tipe selain int

Tentu saja, ini masih berlaku untuk tipe selain int. Anda hanya perlu mengubah jenis pembungkusan AtomicReferencemenjadi larik atau jenis itu. Misalnya, jika Anda menggunakan a String, cukup lakukan hal berikut:

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

Gunakan array:

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

Atau dengan Java 10+:

var wrapper = new Object(){ String value; }
list.forEach(s->{
  wrapper.value = "blah";
});
Olivier Grégoire
sumber
Mengapa Anda diizinkan menggunakan array seperti int[] ordinal = { 0 };? Bisakah Anda menjelaskannya. Terima kasih
mrbela
@mrbela Apa sebenarnya yang tidak kamu mengerti? Mungkin saya bisa mengklarifikasi sedikit itu?
Olivier Grégoire
1
Array @mrbela diteruskan oleh referensi. Saat Anda mengirimkan sebuah array, Anda sebenarnya mengirimkan alamat memori untuk array itu. Tipe primitif seperti integer dikirim oleh nilai, yang berarti salinan nilai diteruskan. Pass-by-value tidak ada hubungan antara nilai asli dan salinan yang dikirim ke metode Anda - memanipulasi salinan tidak berpengaruh apa-apa ke aslinya. Melewati referensi berarti nilai asli dan nilai yang dikirim ke metode adalah satu hal yang sama - memanipulasinya dalam metode Anda akan mengubah nilai di luar metode.
DetectivePikachu
@Olivier Grégoire ini benar-benar menyelamatkan kulit saya. Saya menggunakan solusi Java 10 dengan "var". Bisakah Anda menjelaskan cara kerjanya? Saya telah mencari Google di mana-mana dan ini adalah satu-satunya tempat yang saya temukan dengan penggunaan khusus ini. Dugaan saya adalah bahwa karena lambda hanya mengizinkan (secara efektif) objek akhir dari luar ruang lingkupnya, objek "var" pada dasarnya menipu kompiler untuk menyimpulkan bahwa itu final, karena mengasumsikan bahwa hanya objek akhir yang akan direferensikan di luar ruang lingkup. Saya tidak tahu, itu tebakan terbaik saya. Jika Anda ingin memberikan penjelasan, saya akan menghargainya, karena saya ingin tahu mengapa kode saya berfungsi :)
oaker
1
@oaker Ini berfungsi karena Java akan membuat kelas MyClass $ 1 sebagai berikut: class MyClass$1 { int value; }Di kelas biasa, ini berarti Anda membuat kelas dengan variabel paket-pribadi bernama value. Ini sama, tetapi kelas tersebut sebenarnya anonim bagi kami, bukan bagi kompiler atau JVM. Java akan mengkompilasi kode tersebut seolah-olah itu MyClass$1 wrapper = new MyClass$1();. Dan kelas anonim menjadi kelas lain. Pada akhirnya, kami hanya menambahkan gula sintaksis di atasnya agar dapat dibaca. Juga, kelas tersebut adalah kelas dalam dengan bidang paket-pribadi. Bidang itu bisa digunakan.
Olivier Grégoire
14

Ini cukup dekat dengan masalah XY . Artinya, pertanyaan yang diajukan pada dasarnya adalah bagaimana memutasi variabel lokal yang ditangkap dari lambda. Tetapi tugas sebenarnya yang ada adalah bagaimana memberi nomor pada elemen daftar.

Dalam pengalaman saya, lebih dari 80% dari waktu ada pertanyaan tentang bagaimana mengubah lokal yang ditangkap dari dalam lambda, ada cara yang lebih baik untuk melanjutkan. Biasanya ini melibatkan pengurangan, tetapi dalam kasus ini teknik menjalankan aliran melalui indeks daftar berlaku dengan baik:

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));
Stuart Marks
sumber
3
Solusi yang baik tetapi hanya jika listadalah RandomAccessdaftar
ZhekaKozlov
Saya ingin tahu apakah masalah pesan perkembangan setiap k iterasi dapat dipecahkan secara praktis tanpa penghitung khusus, yaitu, kasus ini: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("Saya telah memproses {} elemen", ctr);} Saya tidak melihat cara yang lebih praktis (reduksi bisa dilakukan, tetapi akan lebih bertele-tele, terutama dengan aliran paralel).
zakmck
14

Jika Anda hanya perlu meneruskan nilai dari luar ke lambda, dan tidak mengeluarkannya, Anda dapat melakukannya dengan kelas anonim biasa alih-alih lambda:

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});
newacct
sumber
1
... dan bagaimana jika Anda benar-benar perlu membaca hasilnya? Hasilnya tidak terlihat oleh kode di tingkat atas.
Luke Usherwood
@ LukeUsherwood: Anda benar. Ini hanya untuk jika Anda hanya perlu meneruskan data dari luar ke lambda, bukan mengeluarkannya. Jika Anda perlu mengeluarkannya, Anda perlu membuat lambda menangkap referensi ke objek yang bisa berubah, misalnya larik, atau objek dengan bidang publik non-final, dan meneruskan data dengan menyetelnya ke dalam objek.
newacct
3

Jika Anda menggunakan Java 10, Anda dapat menggunakannya varuntuk itu:

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

Alternatif untuk AtomicInteger(atau objek lain yang dapat menyimpan nilai) adalah dengan menggunakan array:

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

Tapi lihat jawaban Stuart : mungkin ada cara yang lebih baik untuk menangani kasus Anda.

zakmck
sumber
2

Saya tahu itu pertanyaan lama, tetapi jika Anda merasa nyaman dengan solusinya, mengingat fakta bahwa variabel eksternal harus final, Anda cukup melakukan ini:

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

Mungkin bukan yang paling elegan, atau bahkan yang paling benar, tetapi itu akan berhasil.

Almir Campos
sumber
1

Anda dapat membungkusnya untuk mengatasi kompiler tetapi harap diingat bahwa efek samping di lambda tidak disarankan.

Mengutip javadoc

Efek samping dalam parameter perilaku untuk operasi streaming, secara umum, tidak disarankan, karena sering kali dapat menyebabkan pelanggaran tanpa disadari terhadap persyaratan keadaan tanpa kewarganegaraan Sejumlah kecil operasi aliran, seperti forEach () dan peek (), dapat beroperasi hanya melalui sisi -efek; ini harus digunakan dengan hati-hati

codemonkey
sumber
0

Saya punya masalah yang sedikit berbeda. Alih-alih menambahkan variabel lokal di forEach, saya perlu menetapkan objek ke variabel lokal.

Saya menyelesaikan ini dengan mendefinisikan kelas domain dalam pribadi yang membungkus kedua daftar yang ingin saya ulangi (countryList) dan keluaran yang saya harapkan dari daftar itu (foundCountry). Kemudian menggunakan Java 8 "forEach", saya mengulangi bidang daftar, dan ketika objek yang saya inginkan ditemukan, saya menetapkan objek itu ke bidang keluaran. Jadi ini memberikan nilai ke bidang variabel lokal, tidak mengubah variabel lokal itu sendiri. Saya percaya bahwa karena variabel lokal itu sendiri tidak berubah, kompilator tidak mengeluh. Saya kemudian dapat menggunakan nilai yang saya tangkap di bidang keluaran, di luar daftar.

Objek Domain:

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

Objek pembungkus:

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

Operasi berulang:

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

Anda dapat menghapus metode kelas pembungkus "setCountryList ()" dan membuat kolom "countryList" final, tetapi saya tidak mendapatkan kesalahan kompilasi yang membiarkan detail ini sebagaimana adanya.

Steve T
sumber
0

Untuk mendapatkan solusi yang lebih umum, Anda dapat menulis kelas Wrapper generik:

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

(ini adalah varian dari solusi yang diberikan oleh Almir Campos).

Dalam kasus khusus ini bukan solusi yang baik, karena Integerlebih buruk daripada inttujuan Anda, bagaimanapun solusi ini lebih umum menurut saya.

luca.vercelli
sumber
0

Ya, Anda dapat memodifikasi variabel lokal dari dalam lambda (dengan cara yang ditunjukkan oleh jawaban lain), tetapi Anda tidak boleh melakukannya. Lambda telah digunakan untuk gaya pemrograman fungsional dan ini berarti: Tidak ada efek samping. Apa yang ingin Anda lakukan dianggap gaya yang buruk. Ini juga berbahaya jika terjadi aliran paralel.

Anda harus menemukan solusi tanpa efek samping atau menggunakan for loop tradisional.

Donat
sumber