CodeGym /จาวาบล็อก /สุ่ม /คำอธิบายของแลมบ์ดานิพจน์ในภาษาจาวา พร้อมตัวอย่างและงาน. ส...
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 :)
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION