บทความนี้มีไว้เพื่อใคร?
เนื้อหาไม่ได้อ้างว่ามีคุณค่าทางวิชาการใด ๆ นับประสาอะไรกับความแปลกใหม่ ค่อนข้างตรงกันข้าม: ฉันจะพยายามอธิบายสิ่งต่าง ๆ ที่ซับซ้อน (สำหรับบางคน) ให้เรียบง่ายที่สุด คำขอให้อธิบาย Stream API เป็นแรงบันดาลใจให้ฉันเขียนสิ่งนี้ ฉันคิดเกี่ยวกับเรื่องนี้และตัดสินใจว่าตัวอย่างสตรีมบางส่วนของฉันอาจไม่สามารถเข้าใจได้หากไม่มีความเข้าใจในการแสดงออกของแลมบ์ดา ดังนั้นเราจะเริ่มด้วยการแสดงออกของแลมบ์ดา
- สำหรับผู้ที่อ่านส่วนแรกของบทความนี้
- สำหรับคนที่คิดว่ารู้จัก Java Core ดีอยู่แล้ว แต่ไม่มีเงื่อนงำเกี่ยวกับแลมบ์ดานิพจน์ใน 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()
เมธอดนี้ใช้เพื่อเปลี่ยนสถานะของวัตถุนี้ ค่าของตัวแปรเองจะไม่เปลี่ยนแปลงในขณะที่โปรแกรมกำลังทำงาน มันชี้ไปที่วัตถุเดียวกันเสมอ ซึ่งช่วยให้เราประกาศตัวแปรด้วยคีย์เวิร์ดสุดท้าย นี่คือตัวอย่างเดียวกัน แต่มีนิพจน์แลมบ์ดา:
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
ออบเจกต์ แทน
ไวยากรณ์สำหรับการอ้างอิงเมธอด
มันค่อนข้างง่าย:-
เราส่งการอ้างอิงถึงวิธีการคงที่ดังนี้:
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 } }
-
เราส่งการอ้างอิงไปยังวิธีการที่ไม่คงที่โดยใช้วัตถุที่มีอยู่ เช่นนี้:
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 } }
-
เราส่งการอ้างอิงไปยังเมธอดที่ไม่คงที่โดยใช้คลาสที่นำไปใช้ดังนี้:
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); } } }
-
เราส่งการอ้างอิงไปยังตัวสร้างดังนี้:
ClassName::new
การอ้างอิงเมธอดนั้นสะดวกมากเมื่อคุณมีเมธอดที่จะทำงานเป็นคอลแบ็กได้อย่างสมบูรณ์แบบอยู่แล้ว ในกรณีนี้ แทนที่จะเขียนนิพจน์แลมบ์ดาที่มีโค้ดของเมธอด หรือเขียนนิพจน์แลมบ์ดาที่เรียกเมธอดง่ายๆ เราก็แค่ส่งการอ้างอิงถึงมัน และนั่นแหล่ะ
ความแตกต่างที่น่าสนใจระหว่างคลาสนิรนามและการแสดงออกของแลมบ์ดา
ในคลาสที่ไม่ระบุตัวตนthis
คีย์เวิร์ดจะชี้ไปที่วัตถุของคลาสที่ไม่ระบุตัวตน แต่ถ้าเราใช้สิ่งนี้ในแลมบ์ดา เราจะเข้าถึงอ็อบเจกต์ของคลาสที่มี อันที่เราเขียนการแสดงออกของแลมบ์ดา สิ่งนี้เกิดขึ้นเนื่องจากการแสดงออกของแลมบ์ดาถูกคอมไพล์เป็นเมธอดส่วนตัวของคลาสที่เขียน ฉันไม่แนะนำให้ใช้ "คุณสมบัติ" นี้ เนื่องจากมีผลข้างเคียงและขัดแย้งกับหลักการของการเขียนโปรแกรมเชิงฟังก์ชัน ที่กล่าวว่าแนวทางนี้สอดคล้องกับ OOP อย่างสิ้นเชิง ;)
ฉันได้รับข้อมูลของฉันจากที่ใด และคุณควรอ่านอะไรอีกบ้าง
- บทช่วยสอนบนเว็บไซต์อย่างเป็นทางการของ Oracle ข้อมูลรายละเอียดมากมายพร้อมตัวอย่าง
- บทเกี่ยวกับการอ้างอิงเมธอดในบทช่วยสอน Oracle เดียวกัน
- ดูดเข้าไปในWikipediaหากคุณสงสัยจริงๆ
GO TO FULL VERSION