บทความนี้เหมาะกับใคร?
- สำหรับผู้ที่อ่านส่วนแรกของบทความนี้
- สำหรับผู้ที่คิดว่าตนรู้จัก 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
นั้น
ไวยากรณ์สำหรับการใช้วิธีการอ้างอิง
มันค่อนข้างง่าย:-
ผ่านการอ้างอิงไปยังวิธีการคงที่
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 } }
-
การส่งผ่านการอ้างอิงไปยังวิธีการไม่คงที่โดยใช้วัตถุที่มีอยู่
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 } }
-
เราส่งผ่านการอ้างอิงไปยังวิธีการที่ไม่คงที่โดยใช้คลาสที่มีการใช้วิธีการดังกล่าว
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); } } }
-
การส่งลิงก์ไปยังคอนสตรัคเตอร์
NameКласса::new
การใช้ลิงก์เมธอดนั้นสะดวกมากเมื่อมีเมธอดสำเร็จรูปที่คุณพึงพอใจอย่างยิ่ง และคุณต้องการใช้เป็นช่องทางติดต่อกลับ ในกรณีนี้ แทนที่จะเขียนนิพจน์ lambda ด้วยโค้ดของวิธีการนั้น หรือนิพจน์ lambda ที่เราเรียกวิธีนี้ง่ายๆ เราก็เพียงส่งต่อการอ้างอิงไปยังมัน นั่นคือทั้งหมดที่
ความแตกต่างที่น่าสนใจระหว่างคลาสที่ไม่ระบุชื่อและการแสดงออกของแลมบ์ดา
ในคลาสที่ไม่ระบุชื่อ คีย์เวิร์ดthis
ชี้ไปที่อ็อบเจ็กต์ของคลาสที่ไม่ระบุชื่อนั้น และถ้าเราใช้มันthis
ภายในแลมบ์ดา เราจะสามารถเข้าถึงอ็อบเจ็กต์ของคลาสเฟรมได้ โดยที่เราเขียนพจน์นี้จริงๆ สิ่งนี้เกิดขึ้นเนื่องจากเมื่อคอมไพล์นิพจน์แลมบ์ดาจะกลายเป็นวิธีการส่วนตัวของคลาสที่เขียนนิพจน์เหล่านั้น ฉันจะไม่แนะนำให้ใช้ "คุณสมบัติ" นี้เนื่องจากมีผลข้างเคียงซึ่งขัดแย้งกับหลักการของการเขียนโปรแกรมเชิงฟังก์ชัน แต่แนวทางนี้ค่อนข้างสอดคล้องกับ OOP ;)
ผมไปหาข้อมูลมาจากไหนหรืออ่านอะไรอีก
- บทช่วยสอนบนเว็บไซต์อย่างเป็นทางการของ Oracle รายละเอียดมากมายพร้อมตัวอย่าง แต่เป็นภาษาอังกฤษ
- บทช่วยสอน "Oracle" เดียวกันแต่บทนี้เกี่ยวกับการอ้างอิงวิธีการ
- บทความเกี่ยวกับHabréเกี่ยวกับการเขียนโปรแกรมเชิงฟังก์ชัน (แปลจากบทความอื่น) มีหนังสือหลายเล่มเกี่ยวกับ lambdas เพียงไม่กี่เล่ม เนื่องจากโดยทั่วไปแล้วเป็นเรื่องเกี่ยวกับการเขียนโปรแกรมเชิงฟังก์ชัน
- สำหรับผู้ที่ชอบติดวิกิพีเดีย
- และแน่นอน ฉันพบสิ่งต่างๆ มากมายบน Google :)
GO TO FULL VERSION