ตัวแปรที่ใช้ในแลมบ์ดานิพจน์ควรเป็นขั้นสุดท้ายหรือมีประสิทธิผลสุดท้าย


135

ตัวแปรที่ใช้ในแลมบ์ดานิพจน์ควรเป็นขั้นสุดท้ายหรือมีประสิทธิผลสุดท้าย

เมื่อฉันพยายามใช้calTzมันแสดงข้อผิดพลาดนี้

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            VTimeZone v = (VTimeZone) component;
            v.getTimeZoneId();
            if (calTz == null) {
                calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

5
คุณไม่สามารถปรับเปลี่ยนcalTzจากแลมด้า
Elliott Frisch

2
ฉันคิดว่านี่เป็นหนึ่งในสิ่งเหล่านั้นที่ยังไม่เสร็จทันเวลาสำหรับ Java 8 แต่ Java 8 คือปี 2014 Scala และ Kotlin อนุญาตให้ทำสิ่งนี้มาหลายปีแล้วดังนั้นจึงเป็นไปได้อย่างชัดเจน Java เคยวางแผนที่จะกำจัดข้อ จำกัด แปลก ๆ นี้หรือไม่?
GlenPeterson

5
นี่คือลิงค์ที่อัปเดตสำหรับความคิดเห็นของ @MSDousti
geisterfurz007

ฉันคิดว่าคุณสามารถใช้ Completable Futures เป็นวิธีแก้ปัญหาได้
Kraulain

สิ่งสำคัญอย่างหนึ่งที่ฉันสังเกตเห็น - คุณสามารถใช้ตัวแปรคงที่แทนตัวแปรปกติได้ (สิ่งนี้ทำให้ฉันเดาได้ในที่สุด)
kaushalpranav

คำตอบ:


69

finalหมายถึงตัวแปรที่จะสามารถ instantiated เพียงครั้งเดียว ใน Java คุณไม่สามารถใช้ตัวแปรที่ไม่ใช่ขั้นสุดท้ายในแลมบ์ดาและในคลาสภายในที่ไม่ระบุชื่อได้

คุณสามารถ refactor โค้ดของคุณด้วยลูปสำหรับแต่ละอันเก่า:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    try {
        for(Component component : cal.getComponents().getComponents("VTIMEZONE")) {
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(calTz==null) {
               calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
           }
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

แม้ว่าฉันจะไม่เข้าใจบางส่วนของรหัสนี้:

  • คุณเรียกv.getTimeZoneId();โดยไม่ใช้ค่าส่งคืน
  • ด้วยการมอบหมายcalTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());คุณจะไม่แก้ไขการส่งผ่านเดิมcalTzและคุณไม่ได้ใช้ในวิธีนี้
  • คุณกลับตลอดnullทำไมคุณไม่ตั้งvoidเป็นประเภทผลตอบแทน

หวังว่าเคล็ดลับเหล่านี้จะช่วยให้คุณปรับปรุงได้


เราสามารถใช้ตัวแปรคงที่ไม่ใช่ขั้นสุดท้าย
Narendra Jaggi

93

แม้ว่าคำตอบอื่น ๆ จะพิสูจน์ความต้องการ แต่ก็ไม่ได้อธิบายว่าเหตุใดจึงมีข้อกำหนด

JLS กล่าวถึงเหตุใดใน§15.27.2 :

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

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


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

2
มีความคิดว่าเหตุใดจึง จำกัด เฉพาะตัวแปรในพื้นที่ไม่ใช่สมาชิกชั้นเรียน ฉันพบว่าตัวเองมักจะหลีกเลี่ยงปัญหาโดยการประกาศตัวแปรของฉันเป็นสมาชิกชั้นเรียน ...
David Refaeli

4
สมาชิกคลาส @DavidRefaeli ได้รับการคุ้มครอง / ได้รับผลกระทบจากโมเดลหน่วยความจำซึ่งหากปฏิบัติตามจะให้ผลลัพธ์ที่คาดเดาได้เมื่อแชร์ ตัวแปรท้องถิ่นไม่ได้ระบุไว้ใน§17.4.1
ไดออกซิน

นี่เป็นการแฮ็กโง่ ๆ ซึ่งควรลบออก คอมไพลเลอร์ควรเตือนเกี่ยวกับการเข้าถึงตัวแปรข้ามเธรดที่อาจเกิดขึ้นได้ แต่ควรอนุญาต หรือควรฉลาดพอที่จะรู้ว่าแลมด้าของคุณกำลังทำงานบนเธรดเดียวกันหรือทำงานแบบขนาน ฯลฯ นี่เป็นข้อ จำกัด ที่โง่ ๆ ที่ทำให้ฉันเสียใจ และอย่างที่คนอื่น ๆ พูดถึงปัญหาไม่มีอยู่ในเช่น C #
Josh M.

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

59

จากแลมบ์ดาคุณไม่สามารถอ้างอิงถึงสิ่งที่ยังไม่สิ้นสุดได้ คุณต้องประกาศ wrapper สุดท้ายจากนอก lamda เพื่อเก็บตัวแปรของคุณ

ฉันได้เพิ่มออบเจ็กต์ 'อ้างอิง' สุดท้ายเป็นกระดาษห่อ

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    final AtomicReference<TimeZone> reference = new AtomicReference<>();

    try {
       cal.getComponents().getComponents("VTIMEZONE").forEach(component->{
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(reference.get()==null) {
               reference.set(TimeZone.getTimeZone(v.getTimeZoneId().getValue()));
           }
           });
    } catch (Exception e) {
        //log.warn("Unable to determine ical timezone", e);
    }
    return reference.get();
}   

ฉันคิดเกี่ยวกับแนวทางเดียวกันหรือคล้ายกัน - แต่ฉันต้องการดูคำแนะนำ / ข้อเสนอแนะจากผู้เชี่ยวชาญเกี่ยวกับคำตอบนี้หรือไม่?
YoYo

4
รหัสนี้พลาดค่าเริ่มต้นreference.set(calTz);หรือต้องสร้างการอ้างอิงโดยใช้new AtomicReference<>(calTz)มิฉะนั้น TimeZone ที่ไม่ใช่ค่าว่างที่ระบุเป็นพารามิเตอร์จะสูญหาย
Julien Kronegg

8
นี่น่าจะเป็นคำตอบแรก AtomicReference (หรือคลาส Atomic___ ที่คล้ายกัน) ทำงานกับข้อ จำกัด นี้อย่างปลอดภัยในทุกสถานการณ์ที่เป็นไปได้
GlenPeterson

1
ตกลงนี่ควรเป็นคำตอบที่ยอมรับได้ คำตอบอื่น ๆ ให้ข้อมูลที่เป็นประโยชน์เกี่ยวกับวิธีถอยกลับไปใช้รูปแบบการเขียนโปรแกรมที่ไม่ทำงานและสาเหตุที่ทำ แต่ไม่ได้บอกวิธีแก้ไขปัญหาจริงๆ!
Jonathan Benn

2
@GlenPeterson และเป็นการตัดสินใจที่แย่มากเช่นกันไม่เพียง แต่จะช้าลงมากด้วยวิธีนี้ แต่คุณยังเพิกเฉยต่อคุณสมบัติผลข้างเคียงที่เอกสารกำกับดูแล
ยูจีน

41

Java 8มีแนวคิดใหม่ที่เรียกว่าตัวแปร“ Effectively final” หมายความว่าตัวแปรโลคัลที่ไม่ใช่ขั้นสุดท้ายซึ่งค่าไม่เคยเปลี่ยนแปลงหลังจากการเริ่มต้นเรียกว่า“ ผลสุดท้ายอย่างมีประสิทธิภาพ”

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

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

Java 8ตระหนักถึงความเจ็บปวดจากการประกาศตัวแปรภายในทุกครั้งที่นักพัฒนาใช้แลมด้านำแนวคิดนี้มาใช้และทำให้ไม่จำเป็นต้องทำให้ตัวแปรโลคัลเป็นขั้นสุดท้าย ดังนั้นหากคุณเห็นว่ากฎสำหรับคลาสที่ไม่ระบุตัวตนไม่มีการเปลี่ยนแปลงคุณไม่จำเป็นต้องเขียนfinalคีย์เวิร์ดทุกครั้งเมื่อใช้ lambdas

ฉันพบคำอธิบายที่ดีที่นี่


ควรใช้การจัดรูปแบบโค้ดสำหรับโค้ดเท่านั้นไม่ใช่สำหรับคำศัพท์ทางเทคนิคโดยทั่วไป effectively finalไม่ใช่รหัสมันเป็นคำศัพท์ ดูว่าควรใช้การจัดรูปแบบโค้ดสำหรับข้อความที่ไม่ใช่โค้ดเมื่อใด ในMeta กองมากเกิน
Charles Duffy

(ดังนั้น " finalคำหลัก" จึงเป็นคำของรหัสและถูกต้องเพื่อจัดรูปแบบตามนั้น แต่เมื่อคุณใช้ "ขั้นสุดท้าย" ในเชิงพรรณนาแทนที่จะเป็นรหัสคำศัพท์นั้นจะเป็นคำศัพท์แทน)
Charles Duffy

9

ในตัวอย่างของคุณคุณสามารถแทนที่forEachด้วย lamdba ด้วยforลูปธรรมดาและแก้ไขตัวแปรใด ๆ ได้อย่างอิสระ หรืออาจจะ refactor โค้ดของคุณเพื่อที่คุณจะได้ไม่ต้องแก้ไขตัวแปรใด ๆ อย่างไรก็ตามฉันจะอธิบายเพื่อความครบถ้วนว่าข้อผิดพลาดหมายถึงอะไรและจะแก้ไขอย่างไร

ข้อกำหนดภาษา Java 8, §15.27.2 :

ตัวแปรโลคัลพารามิเตอร์ที่เป็นทางการหรือพารามิเตอร์ข้อยกเว้นใด ๆ ที่ใช้ แต่ไม่ได้ประกาศในนิพจน์แลมบ์ดาจะต้องถูกประกาศขั้นสุดท้ายหรือเป็นขั้นสุดท้ายอย่างมีประสิทธิภาพ ( §4.12.4 ) หรือข้อผิดพลาดเวลาคอมไพล์เกิดขึ้นเมื่อมีการพยายามใช้

โดยทั่วไปคุณไม่สามารถแก้ไขตัวแปรโลคัล ( calTzในกรณีนี้) จากภายในแลมบ์ดา (หรือคลาสโลคัล / ไม่ระบุชื่อ) เพื่อให้บรรลุผลใน Java คุณต้องใช้วัตถุที่เปลี่ยนแปลงได้และแก้ไข (ผ่านตัวแปรสุดท้าย) จากแลมบ์ดา ตัวอย่างหนึ่งของออบเจ็กต์ที่เปลี่ยนแปลงได้ที่นี่จะเป็นอาร์เรย์ขององค์ประกอบเดียว:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    TimeZone[] result = { null };
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            ...
            result[0] = ...;
            ...
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return result[0];
}

อีกวิธีหนึ่งคือการใช้ฟิลด์ของวัตถุ เช่น MyObj result = MyObj ใหม่ (); ... ; result.timeZone = ... ; .... ; ส่งกลับ result.timezone; โปรดทราบว่าตามที่อธิบายไว้ข้างต้นสิ่งนี้ทำให้คุณได้รับปัญหาความปลอดภัยของเธรด ดูstackoverflow.com/a/50341404/7092558
Gibezynu 1

0

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


0

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

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        TimeZone calTzLocal[] = new TimeZone[1];
        calTzLocal[0] = calTz;
        cal.getComponents().get("VTIMEZONE").forEach(component -> {
            TimeZone v = component;
            v.getTimeZoneId();
            if (calTzLocal[0] == null) {
                calTzLocal[0] = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

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