Yerel değişkeni lambda içinden değiştirme


115

Yerel bir değişkeni değiştirme forEach derleme hatası verir:

Normal

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

Lambda ile

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

Bunu nasıl çözeceğine dair bir fikrin var mı?


8
Lambdaların anonim bir iç sınıf için esasen sözdizimsel şeker olduğunu düşünürsek, benim sezgim, son olmayan, yerel bir değişkeni yakalamanın imkansız olduğu yönündedir. Yine de yanlış olduğunun kanıtlanmasını isterim.
Sinkingpoint

2
Lambda ifadesinde kullanılan bir değişken etkili bir şekilde nihai olmalıdır. Aşırı olmasına rağmen atomik bir tam sayı kullanabilirsiniz, bu nedenle burada bir lambda ifadesine gerçekten ihtiyaç yoktur. Sadece for-loop'a bağlı kalın.
Alexis C.

3
Değişken etkili bir şekilde nihai olmalıdır . Şuna bakın: Yerel değişken yakalamada neden kısıtlama var?
Jesper


2
@Quirliom Anonim sınıflar için sözdizimsel şeker değiller. Lambdalar kaputun altında yöntem kulpları kullanıyor
Dioxin

Yanıtlar:


177

Bir sarmalayıcı kullanın

Her çeşit ambalaj iyidir.

Java 8+ ile şunlardan birini kullanın AtomicInteger:

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

... veya bir dizi:

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

İle 10+ Java :

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

Not: Paralel akış kullanıyorsanız çok dikkatli olun. Beklenen sonucu elde edemeyebilirsiniz. Stuart'ınki gibi diğer çözümler bu durumlar için daha uygun olabilir.

Dışındaki türler için int

Elbette bu, dışındaki tipler için hala geçerlidir int. Yalnızca sarma türünü bir AtomicReferenceveya bu türden bir dizi olarak değiştirmeniz gerekir . Örneğin, a kullanıyorsanız String, sadece aşağıdakileri yapın:

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

Bir dizi kullanın:

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

Veya Java 10+ ile:

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

Neden bir dizi kullanmanıza izin veriliyor int[] ordinal = { 0 };? Bunu açıklayabilir misin. Teşekkür ederim
mrbela

@mrbela Tam olarak ne anlamıyorsun? Belki o kısmı netleştirebilirim?
Olivier Grégoire

1
@mrbela dizileri referans olarak aktarılır. Bir diziyi ilettiğinizde, aslında o dizi için bir bellek adresi girmiş olursunuz. Tamsayı gibi ilkel türler, değere göre gönderilir; bu, değerin bir kopyasının aktarıldığı anlamına gelir. Değere göre geçiş, orijinal değer ile yöntemlerinize gönderilen kopya arasında bir ilişki yoktur - kopyayı değiştirmek, orijinalle hiçbir şey yapmaz. Referans yoluyla geçiş, orijinal değer ile yönteme gönderilen değerin aynı olduğu anlamına gelir - yönteminizde onu değiştirmek, yöntemin dışındaki değeri değiştirir.
DetectivePikachu

@Olivier Grégoire bu gerçekten derimi kurtardı. Java 10 çözümünü "var" ile kullandım. Bunun nasıl çalıştığını açıklayabilir misin? Her yerde Google'da araştırdım ve bu özel kullanımla bulduğum tek yer burası. Benim tahminim, bir lambda, kapsamının dışındaki son nesnelere yalnızca (etkin bir şekilde) izin verdiği için, "var" nesnesinin temelde derleyiciyi kandırarak nihai olduğu sonucunu çıkarır, çünkü yalnızca nihai nesnelere kapsam dışında başvurulacağını varsayar. Bilmiyorum, bu benim en iyi tahminim. Bir açıklama yapmak isterseniz çok sevinirim, çünkü
kodumun

1
@oaker Bu işe yarar çünkü Java, MyClass $ 1 sınıfını aşağıdaki gibi oluşturacaktır: class MyClass$1 { int value; }Normal bir sınıfta, bu, adlı paket-özel değişkeni olan bir sınıf oluşturduğunuz anlamına gelir value. Bu aynıdır, ancak sınıf aslında bizim için anonimdir, derleyici veya JVM için değil. Java kodu sanki öyleymiş gibi derleyecektir MyClass$1 wrapper = new MyClass$1();. Ve anonim sınıf, başka bir sınıf haline gelir. Sonunda, okunabilir hale getirmek için üzerine sözdizimsel şeker ekledik. Ayrıca sınıf, özel paket alanına sahip bir iç sınıftır. Bu alan kullanılabilir.
Olivier Gregoire

14

Bu, bir XY sorununa oldukça yakın . Yani, sorulan soru esasen yakalanan bir yerel değişkenin bir lambda'dan nasıl değiştirileceğidir. Ancak asıl görev, bir listenin öğelerinin nasıl numaralandırılacağıdır.

Tecrübelerime göre, zamanın% 80'inin üzerinde, yakalanan bir yerelin bir lambda içinden nasıl mutasyona uğratılacağı sorusu var, ilerlemenin daha iyi bir yolu var. Genellikle bu azaltmayı içerir, ancak bu durumda liste dizinleri üzerinde bir akış çalıştırma tekniği iyi uygulanır:

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

3
İyi bir çözüm ama sadece listbir RandomAccessliste ise
ZhekaKozlov

Her k yinelemede ilerleme mesajı sorununun, özel bir sayaç olmadan pratik olarak çözülüp çözülmeyeceğini merak ediyorum, yani bu durum: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("{} öğeleri işledim", ctr);} Daha pratik bir yol göremiyorum (azaltma bunu yapabilir, ancak özellikle paralel akışlarda daha ayrıntılı olabilir).
zakmck

14

Değeri yalnızca dışarıdan lambdaya geçirmeniz gerekiyorsa ve dışarı çıkarmazsanız, bunu lambda yerine normal bir anonim sınıfla yapabilirsiniz:

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

1
... ve ya sonucu gerçekten okumanız gerekirse? Sonuç, üst düzeydeki kod için görünmez.
Luke Usherwood

@LukeUsherwood: Haklısın. Bu, yalnızca verileri lambdaya dışarıdan aktarmanız değil, dışarıdan aktarmanız gerekiyorsa geçerlidir. Eğer onu çıkarmanız gerekiyorsa, lambda'nın değiştirilebilir bir nesneye, örneğin bir dizi veya nihai olmayan genel alanlara sahip bir nesneye bir başvuru yakalamasını ve verileri nesneye ayarlayarak iletmesini sağlamanız gerekir.
newacct


3

Java 10 kullanıyorsanız var, bunun için kullanabilirsiniz :

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

3

AtomicInteger(Veya bir değeri depolayabilen başka herhangi bir nesneye) bir alternatif, bir dizi kullanmaktır:

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

Ancak Stuart'ın cevabına bakın : Davanızla başa çıkmanın daha iyi bir yolu olabilir.


2

Bunun eski bir soru olduğunu biliyorum, ancak bir geçici çözüm konusunda rahatsanız, harici değişkenin nihai olması gerektiği gerçeğini göz önünde bulundurarak, bunu kolayca yapabilirsiniz:

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

Belki en zarif, hatta en doğru olanı değil, ama işe yarar.


1

Derleyiciye geçici bir çözüm bulmak için bunu tamamlayabilirsiniz, ancak lütfen lambdalarda yan etkilerin tavsiye edilmediğini unutmayın.

Javadoc'tan alıntı yapmak

Davranışsal parametrelerin akış işlemlerine yönelik yan etkileri, genellikle, vatansızlık gerekliliğinin farkında olmadan ihlallerine yol açabileceğinden, genel olarak önerilmez. ForEach () ve peek () gibi az sayıda akış işlemi, yalnızca yan -Etkileri; bunlar dikkatli kullanılmalıdır


0

Biraz farklı bir problemim vardı. ForEach'de yerel bir değişkeni artırmak yerine, yerel değişkene bir nesne atamam gerekiyordu.

Bunu, hem yinelemek istediğim listeyi (countryList) hem de bu listeden almayı umduğum çıktıyı (foundCountry) saran özel bir iç alan sınıfı tanımlayarak çözdüm. Daha sonra Java 8 "forEach" kullanarak liste alanını yineliyorum ve istediğim nesne bulunduğunda bu nesneyi çıktı alanına atıyorum. Bu, yerel değişkenin kendisini değiştirmeden, yerel değişkenin bir alanına bir değer atar. Yerel değişkenin kendisi değişmediği için derleyicinin şikayet etmediğine inanıyorum. Daha sonra çıktı alanında yakaladığım değeri listenin dışında kullanabilirim.

Etki Alanı Nesnesi:

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

Sarmalayıcı nesne:

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

Yineleme işlemi:

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

"SetCountryList ()" sarmalayıcı sınıf yöntemini kaldırıp "countryList" alanını son haline getirebilirsiniz, ancak bu ayrıntıları olduğu gibi bırakarak derleme hataları almadım.


0

Daha genel bir çözüme sahip olmak için, genel bir Wrapper sınıfı yazabilirsiniz:

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

(bu, Almir Campos tarafından verilen çözümün bir çeşididir).

Spesifik durumda bu, amacınızdan Integerdaha kötü olduğu gibi iyi bir çözüm değil int, yine de bu çözümün daha genel olduğunu düşünüyorum.


0

Evet, yerel değişkenleri lambdaların içinden değiştirebilirsiniz (diğer cevapların gösterdiği şekilde), ancak bunu yapmamalısınız. Lambdalar, işlevsel programlama tarzı içindi ve bu şu anlama geliyor: Yan etki yok. Yapmak istediğin şey kötü bir stil olarak görülüyor. Paralel akış durumunda da tehlikelidir.

Ya yan etkileri olmayan bir çözüm bulmalı ya da geleneksel bir for döngüsü kullanmalısınız.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.