โค้ดยิม/จาวาบล็อก/สุ่ม/คำอธิบายของแลมบ์ดานิพจน์ในภาษาจาวา พร้อมตัวอย่างและงาน. ส...
John Squirrels
ระดับ
San Francisco

คำอธิบายของแลมบ์ดานิพจน์ในภาษาจาวา พร้อมตัวอย่างและงาน. ส่วนที่ 2

เผยแพร่ในกลุ่ม
บทความนี้มีไว้เพื่อใคร?
  • สำหรับผู้ที่อ่านส่วนแรกของบทความนี้
  • สำหรับคนที่คิดว่ารู้จัก Java Core ดีอยู่แล้ว แต่ไม่มีเงื่อนงำเกี่ยวกับแลมบ์ดานิพจน์ใน Java หรือบางทีพวกเขาอาจเคยได้ยินบางอย่างเกี่ยวกับการแสดงออกของแลมบ์ดา แต่ขาดรายละเอียด
  • เหมาะสำหรับผู้ที่มีความเข้าใจเกี่ยวกับการแสดงออกของแลมบ์ดา แต่ยังรู้สึกหวาดกลัวและไม่คุ้นเคยกับการใช้พวกมัน
หากคุณไม่เหมาะกับหมวดหมู่ใดหมวดหมู่หนึ่งเหล่านี้ คุณอาจพบว่าบทความนี้น่าเบื่อ มีข้อบกพร่อง หรือโดยทั่วไปแล้วไม่ใช่ถ้วยชาของคุณ ในกรณีนี้ คุณสามารถข้ามไปยังสิ่งอื่นได้ หรือหากคุณเชี่ยวชาญในเรื่องนี้ โปรดแสดงความคิดเห็นในความคิดเห็นว่าฉันจะปรับปรุงหรือเสริมบทความได้อย่างไร คำอธิบายของแลมบ์ดานิพจน์ในภาษาจาวา  พร้อมตัวอย่างและงาน.  ตอนที่ 2 - 1เนื้อหาไม่ได้อ้างว่ามีคุณค่าทางวิชาการใด ๆ นับประสาอะไรกับความแปลกใหม่ ค่อนข้างตรงกันข้าม: ฉันจะพยายามอธิบายสิ่งต่าง ๆ ที่ซับซ้อน (สำหรับบางคน) ให้เรียบง่ายที่สุด คำขอให้อธิบาย Stream API เป็นแรงบันดาลใจให้ฉันเขียนสิ่งนี้ ฉันคิดเกี่ยวกับเรื่องนี้และตัดสินใจว่าตัวอย่างสตรีมบางส่วนของฉันอาจไม่สามารถเข้าใจได้หากไม่มีความเข้าใจในการแสดงออกของแลมบ์ดา ดังนั้นเราจะเริ่มด้วยการแสดงออกของแลมบ์ดา

เข้าถึงตัวแปรภายนอก

รหัสนี้รวบรวมด้วยคลาสที่ไม่ระบุชื่อหรือไม่
int counter = 0;
Runnable r = new Runnable() {

    @Override
    public void run() {
        counter++;
    }
};
ไม่counter ตัวแปรต้องfinalเป็น หรือถ้าไม่finalอย่างน้อยก็เปลี่ยนค่าไม่ได้ หลักการเดียวกันนี้ใช้กับการแสดงออกของแลมบ์ดา พวกเขาสามารถเข้าถึงตัวแปรทั้งหมดที่สามารถ "เห็น" จากตำแหน่งที่ประกาศ แต่แลมบ์ดาจะต้องไม่เปลี่ยนแปลง (กำหนดค่าใหม่ให้กับพวกมัน) อย่างไรก็ตาม มีวิธีหลีกเลี่ยงข้อจำกัดนี้ในคลาสที่ไม่ระบุตัวตน เพียงสร้างตัวแปรอ้างอิงและเปลี่ยนสถานะภายในของวัตถุ ในการทำเช่นนั้น ตัวแปรเองจะไม่เปลี่ยนแปลง (ชี้ไปที่วัตถุเดียวกัน) และสามารถทำเครื่องหมายได้อย่างปลอดภัยfinalเป็น
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {

    @Override
    public void run() {
        counter.incrementAndGet();
    }
};
ที่นี่counterตัวแปรของเราคือการอ้างอิงถึงAtomicIntegerวัตถุ และincrementAndGet()เมธอดนี้ใช้เพื่อเปลี่ยนสถานะของวัตถุนี้ ค่าของตัวแปรเองจะไม่เปลี่ยนแปลงในขณะที่โปรแกรมกำลังทำงาน มันชี้ไปที่วัตถุเดียวกันเสมอ ซึ่งช่วยให้เราประกาศตัวแปรด้วยคีย์เวิร์ดสุดท้าย นี่คือตัวอย่างเดียวกัน แต่มีนิพจน์แลมบ์ดา:
int counter = 0;
Runnable r = () -> counter++;
สิ่งนี้จะไม่คอมไพล์ด้วยเหตุผลเดียวกับเวอร์ชันที่มีคลาสที่ไม่ระบุชื่อ:  counterจะต้องไม่เปลี่ยนแปลงในขณะที่โปรแกรมกำลังทำงาน แต่ทุกอย่างจะดีถ้าเราทำเช่นนี้:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
นอกจากนี้ยังใช้กับวิธีการโทร ภายในแลมบ์ดานิพจน์ คุณไม่เพียงแต่สามารถเข้าถึงตัวแปรที่ "มองเห็นได้" ทั้งหมด แต่ยังเรียกใช้เมธอดที่สามารถเข้าถึงได้อีกด้วย
public class Main {

    public static void main(String[] args) {
        Runnable runnable = () -> staticMethod();
        new Thread(runnable).start();
    }

    private static void staticMethod() {

        System.out.println("I'm staticMethod(), and someone just called me!");
    }
}
แม้ว่าจะstaticMethod()เป็นส่วนตัว แต่ก็สามารถเข้าถึงได้ภายในmain()เมธอด ดังนั้นจึงสามารถเรียกใช้จากภายในแลมบ์ดาที่สร้างขึ้นในเมธอดmainได้

การแสดงออกแลมบ์ดาจะถูกดำเนินการเมื่อใด

คุณอาจพบว่าคำถามต่อไปนี้ง่ายเกินไป แต่คุณควรถามแบบเดิม: โค้ดภายในนิพจน์แลมบ์ดาจะถูกดำเนินการเมื่อใด เมื่อมันถูกสร้าง? หรือเรียกว่า(ซึ่งยังไม่ทราบ) เมื่อใด? การตรวจสอบนี้ค่อนข้างง่าย
System.out.println("Program start");

// All sorts of code here
// ...

System.out.println("Before lambda declaration");

Runnable runnable = () -> System.out.println("I'm a lambda!");

System.out.println("After lambda declaration");

// All sorts of other code here
// ...

System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
เอาต์พุตหน้าจอ:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
คุณจะเห็นว่าการแสดงออกของแลมบ์ดาถูกดำเนินการที่ส่วนท้ายสุด หลังจากสร้างเธรดแล้ว และเมื่อการดำเนินการของโปรแกรมไปถึงเมธอดrun()เท่านั้น ไม่แน่นอนเมื่อมีการประกาศ โดยการประกาศนิพจน์แลมบ์ดา เราได้สร้างวัตถุRunnableและอธิบายวิธีrun()การทำงาน ของมันเท่านั้น วิธีการนั้นถูกดำเนินการในภายหลัง

วิธีการอ้างอิง?

การอ้างอิงเมธอดไม่เกี่ยวข้องโดยตรงกับแลมบ์ดา แต่ฉันคิดว่ามันเหมาะสมที่จะพูดสองสามคำเกี่ยวกับพวกเขาในบทความนี้ สมมติว่าเรามีการแสดงออกของแลมบ์ดาที่ไม่ได้ทำอะไรเป็นพิเศษ แต่เพียงแค่เรียกใช้เมธอด
x -> System.out.println(x)
รับสายบ้างxแค่สายSystem.out.println()ผ่านเข้าxมา ในกรณีนี้ เราสามารถแทนที่ด้วยการอ้างอิงถึงวิธีการที่ต้องการ แบบนี้:
System.out::println
ถูกต้อง — ไม่มีวงเล็บต่อท้าย! นี่คือตัวอย่างที่สมบูรณ์ยิ่งขึ้น:
List<String> strings = new LinkedList<>();

strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");

strings.forEach(x -> System.out.println(x));
ในบรรทัดสุดท้าย เราใช้forEach()เมธอด ซึ่งรับวัตถุที่ใช้Consumerอินเทอร์เฟซ นี่เป็นส่วนต่อประสานการทำงานที่มีเพียงvoid accept(T t)วิธี เดียว ดังนั้น เราจึงเขียนนิพจน์แลมบ์ดาที่มีพารามิเตอร์หนึ่งตัว (เนื่องจากมีการพิมพ์ในอินเทอร์เฟซเอง เราไม่ได้ระบุประเภทพารามิเตอร์ เราระบุเพียงว่าเราจะเรียกมันว่าx) ในเนื้อหาของนิพจน์แลมบ์ดา เราเขียนโค้ดที่จะถูกดำเนินการเมื่อaccept()เรียกใช้เมธอด ที่นี่เราแสดงสิ่งที่ลงท้ายด้วยxตัวแปร เมธอด เดียวกันนี้forEach()วนซ้ำองค์ประกอบทั้งหมดในคอลเล็กชันและเรียกใช้accept()เมธอดในการดำเนินการConsumerอินเทอร์เฟซ (แลมบ์ดาของเรา) ผ่านในแต่ละรายการในคอลเลกชัน อย่างที่ฉันพูด เราสามารถแทนที่นิพจน์แลมบ์ดา (ซึ่งเพียงแค่จัดประเภทเมธอดอื่น) ด้วยการอ้างอิงถึงเมธอดที่ต้องการ จากนั้นรหัสของเราจะมีลักษณะดังนี้:
List<String> strings = new LinkedList<>();

strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");

strings.forEach(System.out::println);
สิ่งสำคัญคือพารามิเตอร์ของprintln()และaccept()เมธอดตรงกัน เนื่องจากprintln()เมธอดสามารถยอมรับอะไรก็ได้ (โอเวอร์โหลดสำหรับประเภทดั้งเดิมและออบเจกต์ทั้งหมด) แทนที่จะใช้แลมบ์ดานิพจน์ เราจึงสามารถส่งการอ้างอิงเมธอดไปprintln()ยัง forEach()จากนั้นforEach()จะนำแต่ละองค์ประกอบในการรวบรวมและส่งต่อไปยังprintln()วิธีการ โดยตรง สำหรับผู้ที่พบสิ่งนี้เป็นครั้งแรก โปรดทราบว่าเราไม่ได้เรียกSystem.out.println()(มีจุดระหว่างคำและวงเล็บในตอนท้าย) เรากำลังส่งการอ้างอิงถึงวิธีการนี้แทน ถ้าเราเขียนสิ่งนี้
strings.forEach(System.out.println());
เราจะมีข้อผิดพลาดในการรวบรวม ก่อนการเรียกไปยังforEach()Java เห็นว่าSystem.out.println()กำลังถูกเรียก ดังนั้นจึงเข้าใจว่าค่าที่ส่งคืนนั้นvoidและจะพยายามส่งผ่านvoidไปforEach()ยัง ซึ่งคาดว่าจะเป็นConsumerออบเจกต์ แทน

ไวยากรณ์สำหรับการอ้างอิงเมธอด

มันค่อนข้างง่าย:
  1. เราส่งการอ้างอิงถึงวิธีการคงที่ดังนี้:ClassName::staticMethodName

    public class Main {
    
        public static void main(String[] args) {
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota");
            strings.add("GTA5");
            strings.add("Halo");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
    
            // Do something
        }
    }
  2. เราส่งการอ้างอิงไปยังวิธีการที่ไม่คงที่โดยใช้วัตถุที่มีอยู่ เช่นนี้:objectName::instanceMethodName

    public class Main {
    
        public static void main(String[] args) {
    
            List<String> strings = new LinkedList<>();
            strings.add("Dota");
            strings.add("GTA5");
            strings.add("Halo");
    
            Main instance = new Main();
            strings.forEach(instance::nonStaticMethod);
        }
    
        private void nonStaticMethod(String s) {
    
            // Do something
        }
    }
  3. เราส่งการอ้างอิงไปยังเมธอดที่ไม่คงที่โดยใช้คลาสที่นำไปใช้ดังนี้:ClassName::methodName

    public class Main {
    
        public static void main(String[] args) {
    
            List<User> users = new LinkedList<>();
            users.add (new User("John"));
            users.add(new User("Paul"));
            users.add(new User("George"));
    
            users.forEach(User::print);
        }
    
        private static class User {
            private String name;
    
            private User(String name) {
                this.name = name;
            }
    
            private void print() {
                System.out.println(name);
            }
        }
    }
  4. เราส่งการอ้างอิงไปยังตัวสร้างดังนี้:ClassName::new

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

ความแตกต่างที่น่าสนใจระหว่างคลาสนิรนามและการแสดงออกของแลมบ์ดา

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

ฉันได้รับข้อมูลของฉันจากที่ใด และคุณควรอ่านอะไรอีกบ้าง

และแน่นอนว่าฉันพบข้อมูลมากมายบน Google :)
ความคิดเห็น
  • เป็นที่นิยม
  • ใหม่
  • เก่า
คุณต้องลงชื่อเข้าใช้เพื่อแสดงความคิดเห็น
หน้านี้ยังไม่มีความคิดเห็นใด ๆ