การแก้ไขตัวแปรโลคัลจากภายในแลมด้า


115

การแก้ไขตัวแปรภายในforEachทำให้เกิดข้อผิดพลาดในการคอมไพล์:

ปกติ

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

ด้วยแลมด้า

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

มีความคิดอย่างไรในการแก้ไขปัญหานี้


8
เมื่อพิจารณาแลมบ์ดาสเป็นน้ำตาลที่ใช้ในการสังเคราะห์สำหรับคลาสภายในที่ไม่ระบุตัวตนสัญชาตญาณของฉันคือเป็นไปไม่ได้ที่จะจับตัวแปรท้องถิ่นที่ไม่ใช่ขั้นสุดท้าย ฉันชอบที่จะพิสูจน์ว่าผิด
Sinkingpoint

2
ตัวแปรที่ใช้ในนิพจน์แลมบ์ดาต้องเป็นขั้นสุดท้ายอย่างมีประสิทธิภาพ คุณสามารถใช้จำนวนเต็มอะตอมได้แม้ว่ามันจะ overkill ดังนั้นจึงไม่จำเป็นต้องใช้นิพจน์แลมบ์ดาที่นี่ เพียงแค่ติดกับห่วง
Alexis C.

3
ตัวแปรต้องเป็นสุดท้ายได้อย่างมีประสิทธิภาพ ดูสิ่งนี้: เหตุใดจึงมีข้อ จำกัด ในการจับตัวแปรภายใน
Jesper


2
@Quirliom พวกเขาไม่ใช่น้ำตาลที่เป็นประโยคสำหรับชั้นเรียนที่ไม่ระบุชื่อ วิธีใช้ Lambdas จับใต้ฝากระโปรง
ไดออกซิน

คำตอบ:


177

ใช้กระดาษห่อหุ้ม

ห่อแบบไหนก็ได้ดี

ด้วยJava 8+ให้ใช้AtomicInteger:

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

... หรืออาร์เรย์:

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

ด้วยJava 10+ :

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

หมายเหตุ: โปรดระวังให้มากหากคุณใช้สตรีมคู่ขนาน คุณอาจไม่ได้ผลลัพธ์ที่คาดหวัง วิธีแก้ปัญหาอื่น ๆ เช่น Stuart's อาจปรับให้เข้ากับกรณีเหล่านั้นได้มากขึ้น

สำหรับประเภทอื่นที่ไม่ใช่ int

ของหลักสูตรนี้ยังคงถูกต้องประเภทอื่น ๆ intกว่า คุณจะต้องเปลี่ยนประเภทการห่อเป็นAtomicReferenceหรืออาร์เรย์ของประเภทนั้นเท่านั้น ตัวอย่างเช่นหากคุณใช้ a Stringให้ทำดังต่อไปนี้:

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

ใช้อาร์เรย์:

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

หรือด้วย Java 10+:

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

ทำไมคุณถึงได้รับอนุญาตให้ใช้อาร์เรย์เช่นint[] ordinal = { 0 };? คุณช่วยอธิบายได้ไหม. ขอบคุณ
mrbela

@mrbela คุณไม่เข้าใจอะไรกันแน่? บางทีฉันอาจจะอธิบายบิตเฉพาะนั้นได้
Olivier Grégoire

1
อาร์เรย์ @mrbela ถูกส่งผ่านโดยการอ้างอิง เมื่อคุณส่งอาร์เรย์คุณกำลังส่งที่อยู่หน่วยความจำสำหรับอาร์เรย์นั้น ประเภทดั้งเดิมเช่นจำนวนเต็มจะถูกส่งโดยค่าซึ่งหมายความว่าจะมีการส่งสำเนาของค่าเข้ามาโดยไม่มีความสัมพันธ์ระหว่างค่าดั้งเดิมกับสำเนาที่ส่งไปยังวิธีการของคุณการจัดการสำเนาจะไม่ทำอะไรกับต้นฉบับ การส่งผ่านโดยการอ้างอิงหมายถึงค่าดั้งเดิมและค่าที่ส่งเข้าไปในเมธอดนั้นเป็นค่าเดียวกันการจัดการในวิธีของคุณจะเปลี่ยนค่าที่อยู่นอกเมธอด
DetectivePikachu

@Olivier Grégoireนี่ช่วยชีวิตฉันได้จริงๆ ฉันใช้โซลูชัน Java 10 กับ "var" คุณสามารถอธิบายวิธีการทำงานนี้ได้หรือไม่? ฉัน Googled ทุกที่และนี่เป็นที่เดียวที่ฉันได้พบกับการใช้งานนี้โดยเฉพาะ ฉันเดาว่าเนื่องจากแลมบ์ดาอนุญาตให้ใช้อ็อบเจ็กต์สุดท้าย (อย่างมีประสิทธิภาพ) จากภายนอกขอบเขตเท่านั้นอ็อบเจ็กต์ "var" จึงหลอกคอมไพเลอร์โดยสรุปว่ามันเป็นขั้นสุดท้ายเนื่องจากถือว่าอ็อบเจ็กต์สุดท้ายเท่านั้นที่จะถูกอ้างอิงนอกขอบเขต ฉันไม่รู้นั่นคือการคาดเดาที่ดีที่สุดของฉัน หากคุณต้องการให้คำอธิบายฉันจะขอบคุณเพราะฉันอยากรู้ว่าทำไมรหัสของฉันถึงใช้งานได้ :)
oaker

1
@oaker งานนี้เพราะ Java จะสร้าง MyClass ระดับ $ 1 ดังนี้ในระดับปกติที่นี้หมายถึงว่าคุณสร้างชั้นที่มีตัวแปรแพคเกจส่วนตัวที่ชื่อclass MyClass$1 { int value; } valueสิ่งนี้เหมือนกัน แต่คลาสนั้นไม่ระบุตัวตนสำหรับเราไม่ใช่คอมไพเลอร์หรือ JVM Java MyClass$1 wrapper = new MyClass$1();จะรวบรวมรหัสราวกับว่ามันเป็น และคลาสที่ไม่ระบุชื่อจะกลายเป็นเพียงคลาสอื่น ในตอนท้ายเราเพิ่งเพิ่มน้ำตาลสังเคราะห์ที่ด้านบนเพื่อให้อ่านได้ นอกจากนี้คลาสยังเป็นคลาสภายในที่มีแพ็กเกจ - ฟิลด์ส่วนตัว ฟิลด์นั้นใช้งานได้
Olivier Grégoire

14

นี้ค่อนข้างใกล้เคียงกับปัญหา XY นั่นคือคำถามที่ถูกถามโดยพื้นฐานแล้วคือวิธีการกลายพันธุ์ตัวแปรท้องถิ่นที่จับได้จากแลมด้า แต่สิ่งที่ต้องทำคือการกำหนดจำนวนองค์ประกอบของรายการ

จากประสบการณ์ของฉันสูงกว่า 80% ของเวลาที่มีคำถามเกี่ยวกับวิธีการกลายพันธุ์ที่จับได้จากภายในแลมด้ามีวิธีที่ดีกว่าในการดำเนินการ โดยปกติจะเกี่ยวข้องกับการลดลง แต่ในกรณีนี้เทคนิคการเรียกใช้สตรีมผ่านดัชนีรายการใช้ได้ดี:

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

3
ทางออกที่ดี แต่ถ้าlistเป็นRandomAccessรายการเท่านั้น
ZhekaKozlov

ฉันอยากรู้ว่าปัญหาของข้อความความคืบหน้าทุกการทำซ้ำ k สามารถแก้ไขได้จริงโดยไม่ต้องใช้ตัวนับเฉพาะหรือไม่เช่นกรณีนี้: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("ฉันประมวลผลแล้ว {} องค์ประกอบ", ctr);} ฉันไม่เห็นวิธีที่เป็นประโยชน์มากกว่านี้ (การลดสามารถทำได้ แต่จะละเอียดกว่าโดยเฉพาะอย่างยิ่งกับสตรีมคู่ขนาน)
zakmck

14

หากคุณต้องการเพียงแค่ส่งค่าจากภายนอกเข้าไปในแลมบ์ดาและไม่สามารถนำออกมาได้คุณสามารถทำได้โดยใช้คลาสที่ไม่ระบุตัวตนปกติแทนแลมด้า:

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

1
... แล้วถ้าต้องอ่านผลจริงๆล่ะ? ผลลัพธ์จะไม่ปรากฏให้เห็นในโค้ดที่ระดับบนสุด
Luke Usherwood

@LukeUsherwood: คุณพูดถูก สิ่งนี้มีไว้สำหรับในกรณีที่คุณต้องการส่งผ่านข้อมูลจากภายนอกเข้าสู่แลมบ์ดาเท่านั้นอย่านำออกไป หากคุณต้องการเอาออกคุณจะต้องให้แลมบ์ดาจับภาพการอ้างอิงไปยังอ็อบเจ็กต์ที่ไม่แน่นอนเช่นอาร์เรย์หรืออ็อบเจกต์ที่มีฟิลด์สาธารณะที่ไม่ใช่ขั้นสุดท้ายและส่งผ่านข้อมูลโดยการตั้งค่าลงในอ็อบเจ็กต์
newacct

7

เนื่องจากตัวแปรที่ใช้จากภายนอก lamda จะต้องเป็นขั้นสุดท้าย (โดยปริยาย) คุณจึงต้องใช้บางสิ่งเช่นAtomicIntegerหรือเขียนโครงสร้างข้อมูลของคุณเอง

ดู https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#accessing-local-variables


3

หากคุณใช้ Java 10 คุณสามารถใช้varเพื่อ:

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

3

อีกทางเลือกหนึ่งสำหรับAtomicInteger(หรือวัตถุอื่น ๆ ที่สามารถเก็บค่าได้) คือการใช้อาร์เรย์:

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

แต่ดูคำตอบของ Stuart : อาจมีวิธีที่ดีกว่าในการจัดการกับกรณีของคุณ


2

ฉันรู้ว่านั่นเป็นคำถามเก่า แต่ถ้าคุณพอใจกับวิธีแก้ปัญหาโดยพิจารณาจากข้อเท็จจริงที่ว่าตัวแปรภายนอกควรเป็นขั้นสุดท้ายคุณสามารถทำได้:

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

อาจจะไม่สวยหรูที่สุดหรือถูกต้องที่สุด แต่ก็ทำได้


1

คุณสามารถสรุปได้เพื่อแก้ไขปัญหาคอมไพเลอร์ แต่โปรดจำไว้ว่าผลข้างเคียงใน lambdas ไม่ได้รับการสนับสนุน

เพื่ออ้างถึงjavadoc

โดยทั่วไปผลข้างเคียงในพารามิเตอร์พฤติกรรมต่อการดำเนินการสตรีมเป็นสิ่งที่ไม่พึงปรารถนาเนื่องจากมักนำไปสู่การละเมิดข้อกำหนดการไร้สัญชาติโดยไม่เจตนาการดำเนินการสตรีมจำนวนเล็กน้อยเช่น forEach () และ peek () สามารถทำงานผ่านด้านข้างเท่านั้น - ผลกระทบ; ควรใช้ด้วยความระมัดระวัง


0

ฉันมีปัญหาที่แตกต่างกันเล็กน้อย แทนที่จะเพิ่มตัวแปรโลคัลใน forEach ฉันต้องกำหนดอ็อบเจกต์ให้กับตัวแปรโลคัล

ฉันแก้ไขสิ่งนี้โดยกำหนดคลาสโดเมนภายในส่วนตัวที่รวมทั้งรายการที่ฉันต้องการวนซ้ำ (countryList) และผลลัพธ์ที่ฉันหวังว่าจะได้รับจากรายการนั้น (foundCountry) จากนั้นใช้ Java 8 "forEach" ฉันวนซ้ำในฟิลด์รายการและเมื่อพบวัตถุที่ฉันต้องการฉันกำหนดอ็อบเจ็กต์นั้นให้กับฟิลด์เอาต์พุต ดังนั้นจึงกำหนดค่าให้กับฟิลด์ของตัวแปรโลคัลโดยไม่เปลี่ยนตัวแปรโลคัลเอง ฉันเชื่อว่าเนื่องจากไม่มีการเปลี่ยนแปลงตัวแปรภายในคอมไพเลอร์จึงไม่บ่น จากนั้นฉันสามารถใช้ค่าที่ฉันจับได้ในฟิลด์เอาต์พุตนอกรายการ

วัตถุโดเมน:

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:

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

ดำเนินการซ้ำ:

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

คุณสามารถลบเมธอดคลาส wrapper "setCountryList ()" และทำให้ฟิลด์ "countryList" เป็นขั้นสุดท้าย แต่ฉันไม่ได้รับข้อผิดพลาดในการคอมไพล์โดยทิ้งรายละเอียดเหล่านี้ไว้ตามที่เป็นอยู่


0

หากต้องการวิธีแก้ปัญหาทั่วไปคุณสามารถเขียนคลาส 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++;
});

(นี่คือตัวแปรของการแก้ปัญหาที่ Almir Campos กำหนด)

ในกรณีเฉพาะนี่ไม่ใช่วิธีแก้ปัญหาที่ดี แต่Integerแย่กว่าintสำหรับจุดประสงค์ของคุณอย่างไรก็ตามฉันคิดว่าวิธีนี้เป็นวิธีที่กว้างกว่า


0

ได้คุณสามารถแก้ไขตัวแปรภายในจาก lambdas ภายใน (ตามวิธีที่แสดงโดยคำตอบอื่น ๆ ) แต่คุณไม่ควรทำเช่นนั้น Lambdas เป็นรูปแบบการเขียนโปรแกรมที่ใช้งานได้จริงและหมายความว่า: ไม่มีผลข้างเคียง สิ่งที่คุณต้องการทำถือเป็นรูปแบบที่ไม่ดี นอกจากนี้ยังเป็นอันตรายในกรณีของสายน้ำคู่ขนาน

คุณควรหาวิธีแก้ปัญหาโดยไม่มีผลข้างเคียงหรือใช้แบบเดิมสำหรับการวนซ้ำ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.