มีประโยชน์ด้านประสิทธิภาพการใช้ไวยากรณ์อ้างอิงวิธีแทนไวยากรณ์แลมบ์ดาใน Java 8 หรือไม่


56

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

อ้างอิงจากการสอนของ Java เกี่ยวกับวิธีการอ้างอิง :

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

ฉันชอบไวยากรณ์แลมบ์ดากับไวยากรณ์การอ้างอิงเมธอดด้วยเหตุผลหลายประการ:

แลมบ์ดานั้นชัดเจนขึ้น

แม้จะมีการอ้างสิทธิ์ของออราเคิล แต่ฉันพบว่าแลมบ์ดาไวยากรณ์นั้นง่ายต่อการอ่านมากกว่าการอ้างอิงวิธีการออบเจ็กต์สั้น ๆ เพราะไวยากรณ์การอ้างอิงเมธอดนั้นไม่ชัดเจน:

Bar::foo

คุณกำลังเรียกวิธีการหนึ่งอาร์กิวเมนต์คงที่ในชั้นเรียนของ x และผ่านมันได้หรือไม่

x -> Bar.foo(x)

หรือคุณกำลังเรียกวิธีการเป็นศูนย์อาร์กิวเมนต์บน x?

x -> x.foo()

ไวยากรณ์การอ้างอิงเมธอดสามารถยืนอยู่ได้ทั้งอันใดอันหนึ่ง มันซ่อนสิ่งที่รหัสของคุณกำลังทำอยู่

แลมบ์ดาปลอดภัยกว่า

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

คุณสามารถใช้ lambdas อย่างสม่ำเสมอ

คุณสามารถห่อฟังก์ชั่นใด ๆ ในแลมบ์ดา - เพื่อให้คุณสามารถใช้ไวยากรณ์เดียวกันได้ทุกที่ ไวยากรณ์การอ้างอิงเมธอดจะไม่ทำงานกับวิธีการที่ใช้หรือคืนค่าอาร์เรย์แบบดั้งเดิมโยนข้อยกเว้นที่ตรวจสอบหรือมีชื่อวิธีการเดียวกับที่ใช้เป็นอินสแตนซ์และวิธีการแบบสแตติก (เนื่องจากไวยากรณ์การอ้างอิงเมธอดนั้นไม่ชัดเจน . มันไม่ทำงานเมื่อคุณมีเมธอดมากเกินไปด้วยจำนวนอาร์กิวเมนต์ที่เท่ากัน แต่คุณไม่ควรทำเช่นนั้น (ดูรายการของ Josh Bloch ที่ 41) ดังนั้นเราจึงไม่สามารถระงับวิธีการดังกล่าวได้

ข้อสรุป

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

PS

ไม่ว่าที่นี่หรือที่นั่น แต่ในความฝันของฉันการอ้างอิงวิธีวัตถุมีลักษณะเช่นนี้มากขึ้นและใช้การเรียกใช้แบบไดนามิกกับวิธีการโดยตรงบนวัตถุโดยไม่มีตัวห่อหุ้มแลมบ์ดา:

_.foo()

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

9
ฉันคิดว่าคุณกังวลเกี่ยวกับประสิทธิภาพก่อนเวลาอันควร ในความเห็นของฉันควรจะมีการพิจารณารองสำหรับการใช้วิธีการอ้างอิง; มันคือทั้งหมดที่เกี่ยวกับการแสดงเจตนาของคุณ หากคุณต้องการส่งผ่านฟังก์ชั่นบางอย่างทำไมไม่เพียงแค่ส่งผ่านฟังก์ชั่นแทนฟังก์ชั่นอื่น ๆ ที่ได้รับมอบหมาย? มันแปลกสำหรับฉัน เหมือนกับที่คุณไม่ได้ซื้อเลยฟังก์ชั่นนั้นเป็นสิ่งที่ดีเลิศพวกเขาต้องการผู้ดูแลที่ไม่ระบุชื่อเพื่อย้ายพวกมันไปมา แต่เพื่อตอบคำถามของคุณโดยตรงฉันจะไม่แปลกใจถ้าการอ้างอิงวิธีการสามารถใช้เป็นการโทรคงที่
Doval

7
.map(_.acceptValue()): ถ้าฉันไม่ผิดนี่ดูเหมือนว่าจะเป็นไวยากรณ์ของสกาล่า บางทีคุณอาจกำลังพยายามใช้ภาษาผิด
Giorgio

2
"ไวยากรณ์การอ้างอิงเมธอดจะไม่ทำงานในวิธีที่คืนค่าเป็นโมฆะ" - เป็นอย่างไร ฉันจะผ่านSystem.out::printlnไปforEach()ตลอดเวลา ... ?
Lukas Eder

3
"ไวยากรณ์การอ้างอิงเมธอดจะไม่ทำงานกับวิธีการที่ใช้หรือส่งคืนอาร์เรย์ดั้งเดิมส่งข้อยกเว้นที่ตรวจสอบแล้ว .... "นั่นไม่ถูกต้อง คุณสามารถใช้การอ้างอิงเมธอดเช่นเดียวกับการแสดงออกแลมบ์ดาสำหรับกรณีดังกล่าวตราบใดที่อินเทอร์เฟซการทำงานที่เป็นปัญหาอนุญาตให้ทำได้
Stuart Marks

คำตอบ:


12

ในหลาย ๆ สถานการณ์ฉันคิดว่าแลมบ์ดาและวิธีการอ้างอิงนั้นเทียบเท่ากัน แต่แลมบ์ดาจะห่อเป้าหมายการร้องขอตามประเภทอินเตอร์เฟสที่ประกาศไว้

ตัวอย่างเช่น

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

คุณจะเห็นคอนโซลเอาท์พุท stacktrace

ในlambda()การเรียกใช้เมธอดtarget()คือlambda$lambda$0(InvokeTest.java:20)ซึ่งมีข้อมูลบรรทัดที่สอบกลับได้ เห็นได้ชัดว่านั่นคือแลมบ์ดาที่คุณเขียนคอมไพเลอร์สร้างวิธีที่ไม่ระบุตัวตนสำหรับคุณ แล้วโทรของวิธีการแลมบ์ดาเป็นสิ่งที่ต้องการInvokeTest$$Lambda$2/1617791695.run(Unknown Source)นั่นคือinvokedynamicการโทรใน JVM ก็หมายความว่าสายจะเชื่อมโยงกับวิธีการสร้าง

ในmethodReference()โทรวิธีการที่target()จะโดยตรงInvokeTest$$Lambda$1/758529971.run(Unknown Source)ก็หมายความว่าการเรียกร้องมีการเชื่อมโยงโดยตรงกับInvokeTest::targetวิธีการ

ข้อสรุป

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


1
คำตอบด้านล่างเกี่ยวกับ metafactory นั้นดีขึ้นเล็กน้อย ฉันเพิ่มความคิดเห็นที่เป็นประโยชน์บางอย่างที่เกี่ยวข้องกับมัน (เช่นวิธีการใช้งานเช่น Oracle / OpenJDK ใช้ ASM เพื่อสร้างวัตถุคลาส wrapper ที่จำเป็นในการสร้างแลมบ์ดา / วิธีการจัดการอินสแตนซ์
Ajax

29

มันคือทั้งหมดที่เกี่ยวกับmetafactory

ขั้นแรกการอ้างอิงวิธีการส่วนใหญ่ไม่ต้องการ desugaring โดย lambda metafactory พวกเขาจะใช้เป็นวิธีการอ้างอิงเพียงอย่างเดียว ภายใต้หัวข้อ "บทความแลมบ์ดาฟ้องร้อง" ของบทความแปลแลมบ์ดานิพจน์ ("TLE"):

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

นี่คือไฮไลต์เพิ่มเติมต่อไปใน "The Lambda Metafactory" ของ TLE:

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

implอาร์กิวเมนต์ระบุวิธีการที่แลมบ์ดาทั้งร่างกายแลมบ์ดา desugared หรือวิธีการที่มีชื่อในการอ้างอิงวิธี

คงที่ ( Integer::sum) หรือวิธีการเช่นมากมาย ( Integer::intValue) อ้างอิงเป็น 'ง่าย' หรือมากที่สุด 'สะดวก' ในความรู้สึกที่ว่าพวกเขาสามารถจัดการได้อย่างเหมาะสมโดยเร็วเส้นทาง ' ตัวแปร metafactory โดยไม่ต้อง desugaring ข้อได้เปรียบนี้ชี้ให้เห็นอย่างเป็นประโยชน์ใน "ตัวแปร Metafactory" ของ TLE:

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

โดยปกติแล้วการอ้างอิงวิธีการจับภาพอินสแตนซ์ ( obj::myMethod) จำเป็นต้องจัดเตรียมอินสแตนซ์ที่ถูกผูกไว้เป็นอาร์กิวเมนต์สำหรับการจัดการเมธอดสำหรับการเรียกใช้ซึ่งอาจหมายถึงความต้องการในการแยกข้อมูลโดยใช้วิธีการ 'บริดจ์'

ข้อสรุป

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


1
นี่คือคำตอบที่เกี่ยวข้องมากที่สุดเนื่องจากสัมผัสกับเครื่องจักรภายในที่จำเป็นในการสร้างแลมบ์ดา ในฐานะที่เป็นคนที่ดีบั๊กกระบวนการสร้างแลมบ์ดา (เพื่อที่จะหาวิธีสร้างรหัสที่เสถียรสำหรับการอ้างอิงแลมบ์ดา / วิธีการที่กำหนดเพื่อให้พวกเขาสามารถลบออกจากแผนที่ได้) ฉันพบว่ามันค่อนข้างน่าแปลกใจ ตัวอย่างของแลมบ์ดา Oracle / OpenJDK ใช้ ASM เพื่อสร้างคลาสแบบไดนามิกซึ่งหากจำเป็นให้ปิดตัวแปรที่อ้างอิงในแลมบ์ดาของคุณ (หรือตัวระบุอินสแตนซ์ของการอ้างอิงเมธอด) ...
Ajax

1
... นอกเหนือจากการปิดตัวแปรแลมบ์ดาโดยเฉพาะอย่างยิ่งยังสร้างวิธีการแบบคงที่ที่ถูกฉีดเข้าไปในคลาสที่ประกาศดังนั้นแลมบ์ดาที่สร้างขึ้นมีบางสิ่งที่จะเรียกแผนที่ในฟังก์ชั่นที่คุณต้องการเรียกใช้ การอ้างอิงวิธีการที่มีตัวระบุอินสแตนซ์จะใช้อินสแตนซ์นั้นเป็นพารามิเตอร์ของวิธีนี้ ฉันยังไม่ได้ทดสอบว่าการอ้างอิงเมธอด :: ในคลาสส่วนตัวข้ามการปิดนี้หรือไม่ แต่ฉันได้ทดสอบว่าวิธีสแตติกทำจริงๆแล้วข้ามเมธอดกลาง (และเพิ่งสร้างวัตถุเพื่อให้สอดคล้องกับเป้าหมายการร้องขอที่ ไซต์โทร)
Ajax

1
สันนิษฐานว่าเป็นวิธีการส่วนตัวไม่จำเป็นต้องมีการจัดส่งเสมือนจริงในขณะที่สิ่งใด ๆ ที่สาธารณะจะต้องปิดอินสแตนซ์ (ผ่านวิธีการคงที่สร้างขึ้น) เพื่อที่จะสามารถเรียกใช้เสมือนบนอินสแตนซ์ที่เกิดขึ้นจริงเพื่อให้แน่ใจว่า TL; DR: การอ้างอิงเมธอดปิดอาร์กิวเมนต์ที่น้อยลงดังนั้นมันจึงรั่วน้อยลงและต้องการการสร้างโค้ดแบบไดนามิกที่น้อยลง
Ajax

3
ความคิดเห็นสแปมครั้งสุดท้าย ... การสร้างคลาสแบบไดนามิกนั้นค่อนข้างหนัก ยังคงดีกว่าการโหลดคลาสนิรนามใน classloader (เนื่องจากชิ้นส่วนที่หนักที่สุดทำได้เพียงครั้งเดียว) แต่คุณจะประหลาดใจจริง ๆ ว่ามีงานมากแค่ไหนในการสร้างอินสแตนซ์แลมบ์ดา มันจะน่าสนใจที่จะเห็นมาตรฐานที่แท้จริงของ lambdas เทียบกับวิธีอ้างอิงกับคลาสที่ไม่ระบุชื่อ โปรดทราบว่าสอง lambdas หรือวิธีการอ้างอิงที่อ้างอิงสิ่งเดียวกัน แต่ในสถานที่ต่างกันสร้างคลาสที่แตกต่างกันที่ runtime (แม้ภายในวิธีเดียวกันทันทีหลังจากกันและกัน)
Ajax

@ Ajax แล้วอันนี้ล่ะ? blog.soat.fr/2015/12/benchmark-java-lambda-vs-classe-anonyme
Walfrat

0

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

เมื่อคุณประกาศแลมบ์ดานิพจน์คุณกำลังสร้างการปิดขอบเขตท้องถิ่น

สิ่งนี้หมายความว่าอะไรและมีผลต่อประสิทธิภาพอย่างไร

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

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


กันไปสำหรับการอ้างอิงวิธีการไม่คงที่
Ajax

12
Java lambdas ไม่ได้ปิดอย่างเข้มงวด ไม่ว่าจะเป็นคลาสภายในแบบไม่ระบุชื่อ พวกเขาไม่ได้อ้างอิงถึงตัวแปรทั้งหมดในขอบเขตที่พวกเขาจะประกาศ พวกเขามีการอ้างอิงถึงตัวแปรที่พวกเขาอ้างอิงจริงเท่านั้น นอกจากนี้ยังใช้กับthisวัตถุที่สามารถอ้างอิงได้ แลมบ์ดาจึงไม่รั่วไหลของทรัพยากรเพียงแค่มีขอบเขตสำหรับพวกมันเท่านั้น มันเก็บเฉพาะกับวัตถุที่ต้องการ ในทางตรงกันข้ามคลาสภายในที่ไม่ระบุชื่อสามารถรั่วไหลของทรัพยากร แต่ไม่ได้ทำเช่นนั้นเสมอไป ดูตัวอย่างโค้ดนี้: a.blmq.us/2mmrL6v
squid314

คำตอบนั้นผิดไปหมด
Stefan Reich

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