JavaRush /จาวาบล็อก /Random-TH /เป็นที่นิยมเกี่ยวกับนิพจน์แลมบ์ดาใน Java พร้อมตัวอย่างและ...
Стас Пасинков
ระดับ
Киев

เป็นที่นิยมเกี่ยวกับนิพจน์แลมบ์ดาใน Java พร้อมตัวอย่างและงาน ส่วนที่ 2

เผยแพร่ในกลุ่ม
บทความนี้เหมาะกับใคร?
  • สำหรับผู้ที่อ่านส่วนแรกของบทความนี้

  • สำหรับผู้ที่คิดว่าตนรู้จัก Java Core ดีอยู่แล้ว แต่ไม่มีความรู้เกี่ยวกับ lambda expression ใน Java หรือบางทีคุณอาจเคยได้ยินบางอย่างเกี่ยวกับแลมบ์ดามาแล้วแต่ไม่มีรายละเอียด

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

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

รหัสนี้จะคอมไพล์ด้วยคลาสที่ไม่ระบุชื่อหรือไม่?
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()และหากต้องการเปลี่ยนสถานะของออบเจ็ กต์นี้ จะใช้เมธอด ค่าของตัวแปรนั้นจะไม่เปลี่ยนแปลงในขณะที่โปรแกรมกำลังทำงานและจะชี้ไปที่ออบเจ็กต์เดียวกันเสมอ ซึ่งช่วยให้เราสามารถประกาศตัวแปรได้ทันทีด้วยfinalคีย์เวิร์ด ตัวอย่างเดียวกัน แต่มีนิพจน์แลมบ์ดา:
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("Я - метод staticMethod(), и меня только-что кто-то вызвал!");
    }
}
แม้ว่าวิธีการนี้staticMethod()จะเป็นแบบส่วนตัว แต่ก็สามารถ "เข้าถึงได้" ที่จะเรียกภายในวิธีการmain()ดังนั้นจึงสามารถเข้าถึงการโทรจากภายในแลมบ์ดาที่สร้างขึ้นในวิธีการได้เช่นmainกัน

ช่วงเวลาของการดำเนินการโค้ดนิพจน์แลมบ์ดา

คำถามนี้อาจดูง่ายเกินไปสำหรับคุณ แต่ก็คุ้มค่าที่จะถาม: เมื่อใดที่โค้ดในนิพจน์แลมบ์ดาจะถูกดำเนินการ? ในช่วงเวลาแห่งการสร้างสรรค์? หรือเมื่อไร (ยังไม่รู้ว่าที่ไหน) จะถูกเรียก? มันค่อนข้างง่ายที่จะตรวจสอบ
System.out.println("Запуск программы");

// много всякого разного codeа
// ...

System.out.println("Перед объявлением лямбды");

Runnable runnable = () -> System.out.println("Я - лямбда!");

System.out.println("После объявления лямбды");

// много всякого другого codeа
// ...

System.out.println("Перед передачей лямбды в тред");
new Thread(runnable).start();
เอาต์พุตบนจอแสดงผล:
Запуск программы
Перед объявлением лямбды
После объявления лямбды
Перед передачей лямбды в тред
Я - лямбда!
จะเห็นได้ว่าโค้ดนิพจน์แลมบ์ดาถูกดำเนินการในตอนท้ายสุด หลังจากสร้างเธรดแล้ว และเฉพาะเมื่อกระบวนการเรียกใช้โปรแกรมถึงการดำเนินการจริงของเมธอดrun()เท่านั้น และไม่ใช่เลยในขณะที่ประกาศ ด้วยการประกาศนิพจน์แลมบ์ดา เราจะสร้างเฉพาะวัตถุประเภทนั้นRunnableและอธิบายลักษณะการทำงานของวิธีการนั้นrun()เท่านั้น วิธีการนี้เปิดตัวในภายหลังมาก

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

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

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

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

ไวยากรณ์สำหรับการใช้วิธีการอ้างอิง

มันค่อนข้างง่าย:
  1. ผ่านการอ้างอิงไปยังวิธีการคงที่NameКласса:: NameСтатическогоМетода?

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            strings.forEach(Main::staticMethod);
        }
    
        private static void staticMethod(String s) {
            // do something
        }
    }
  2. การส่งผ่านการอ้างอิงไปยังวิธีการไม่คงที่โดยใช้วัตถุที่มีอยู่NameПеременнойСОбъектом:: method name

    public class Main {
        public static void main(String[] args) {
            List<String> strings = new LinkedList<>();
            strings.add("Mother");
            strings.add("soap");
            strings.add("frame");
    
            Main instance = new Main();
            strings.forEach(instance::nonStaticMethod);
        }
    
        private void nonStaticMethod(String s) {
            // do something
        }
    }
  3. เราส่งผ่านการอ้างอิงไปยังวิธีการที่ไม่คงที่โดยใช้คลาสที่มีการใช้วิธีการดังกล่าวNameКласса:: method name

    public class Main {
        public static void main(String[] args) {
            List<User> users = new LinkedList<>();
            users.add(new User("Vasya"));
            users.add(new User("Коля"));
            users.add(new User("Петя"));
    
            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. การส่งลิงก์ไปยังคอนสตรัคเตอร์NameКласса::new
    การใช้ลิงก์เมธอดนั้นสะดวกมากเมื่อมีเมธอดสำเร็จรูปที่คุณพึงพอใจอย่างยิ่ง และคุณต้องการใช้เป็นช่องทางติดต่อกลับ ในกรณีนี้ แทนที่จะเขียนนิพจน์ lambda ด้วยโค้ดของวิธีการนั้น หรือนิพจน์ lambda ที่เราเรียกวิธีนี้ง่ายๆ เราก็เพียงส่งต่อการอ้างอิงไปยังมัน นั่นคือทั้งหมดที่

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

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

ผมไปหาข้อมูลมาจากไหนหรืออ่านอะไรอีก

ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION