ในตอนแรก Java เป็นภาษาเชิงวัตถุโดยสมบูรณ์ ยกเว้นประเภทดั้งเดิม ทุกอย่างใน Java จะเป็นอ็อบเจ็กต์ แม้แต่อาร์เรย์ก็เป็นวัตถุ อินสแตนซ์ของแต่ละคลาสเป็นวัตถุ ไม่มีความเป็นไปได้แม้แต่ครั้งเดียวที่จะกำหนดฟังก์ชันแยกจากกัน (นอกคลาส - โดยประมาณ transl. ) และไม่มีวิธีใดที่จะส่งเมธอดเป็นอาร์กิวเมนต์หรือส่งคืนเนื้อความของเมธอดอันเป็นผลมาจากเมธอดอื่นได้ ประมาณนั้นแหละ. แต่นี่เป็นก่อน Java 8
ตั้งแต่สมัยของ Swing เก่า ๆ จำเป็นต้องเขียนคลาสที่ไม่ระบุชื่อเมื่อจำเป็นต้องส่งฟังก์ชันบางอย่างไปยังวิธีการบางอย่าง ตัวอย่างเช่น นี่คือลักษณะของการเพิ่มตัวจัดการเหตุการณ์:
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
//Event listener implementation goes here...
}
});
ที่นี่เราต้องการเพิ่มโค้ดบางส่วนให้กับตัวฟังเหตุการณ์ของเมาส์ เรากำหนดคลาสที่ไม่ระบุชื่อMouseAdapter
และสร้างวัตถุจากคลาสนั้นทันที ด้วยวิธีนี้ เราได้ส่งผ่านฟังก์ชันเพิ่มเติมไปยังaddMouseListener
. กล่าวโดยสรุป มันไม่ง่ายเลยที่จะส่งวิธีง่ายๆ (ฟังก์ชันการทำงาน) ใน Java ผ่านอาร์กิวเมนต์ ข้อจำกัดนี้บังคับให้นักพัฒนา Java 8 เพิ่มคุณสมบัติ เช่น นิพจน์ Lambda ให้กับข้อกำหนดภาษา
เหตุใด Java จึงต้องการนิพจน์ Lambda
จากจุดเริ่มต้น ภาษา Java ไม่ได้มีการพัฒนามากนัก ยกเว้นสิ่งต่างๆ เช่น Annotations, Generics และอื่นๆ ประการแรก Java ยังคงเป็น Object-Oriented อยู่เสมอ หลังจากทำงานกับภาษาเชิงฟังก์ชันเช่น JavaScript แล้ว เราจะเข้าใจได้ว่า Java เป็นแบบเชิงวัตถุและพิมพ์อย่างเข้มงวดอย่างไร ไม่จำเป็นต้องใช้ฟังก์ชันใน Java ด้วยตัวมันเอง พวกมันไม่สามารถพบได้ในโลก Java ในภาษาโปรแกรมเชิงฟังก์ชัน ฟังก์ชันต่างๆ จะมาก่อน พวกมันมีอยู่ด้วยตัวของมันเอง คุณสามารถกำหนดให้กับตัวแปรและส่งผ่านอาร์กิวเมนต์ไปยังฟังก์ชันอื่นได้ JavaScript เป็นหนึ่งในตัวอย่างที่ดีที่สุดของภาษาโปรแกรมเชิงฟังก์ชัน คุณสามารถค้นหาบทความดีๆ บนอินเทอร์เน็ตที่ให้รายละเอียดเกี่ยวกับประโยชน์ของ JavaScript ในฐานะภาษาที่ใช้งานได้ ภาษาที่ใช้งานได้มีเครื่องมืออันทรงพลังเช่นการปิดซึ่งมีข้อได้เปรียบเหนือวิธีการเขียนแอปพลิเคชันแบบเดิมหลายประการ การปิดคือฟังก์ชันที่มีสภาพแวดล้อมติดอยู่ ซึ่งเป็นตารางที่เก็บการอ้างอิงถึงตัวแปรที่ไม่ใช่ในเครื่องทั้งหมดของฟังก์ชัน ใน Java การปิดสามารถจำลองได้ผ่านนิพจน์ Lambda แน่นอนว่ามีความแตกต่างระหว่างการปิดและนิพจน์ Lambda ไม่ใช่ความแตกต่างเล็กๆ น้อยๆ แต่นิพจน์ lambda เป็นทางเลือกที่ดีในการปิด ในบล็อกที่เสียดสีและตลกขบขัน Steve Yegge อธิบายว่าโลกของ Java เชื่อมโยงกับคำนามอย่างเคร่งครัดอย่างไร (เอนทิตี วัตถุ - ประมาณการแปล ) หากคุณยังไม่ได้อ่านบล็อกของเขาฉันขอแนะนำ เขาอธิบายเหตุผลที่แน่ชัดว่าทำไมนิพจน์ Lambda ถึงถูกเพิ่มลงใน Java ด้วยวิธีที่ตลกและน่าสนใจ นิพจน์ Lambda นำฟังก์ชันการทำงานมาสู่ Java ที่ขาดหายไปมานาน นิพจน์ Lambda นำฟังก์ชันการทำงานมาสู่ภาษาเช่นเดียวกับวัตถุ แม้ว่าสิ่งนี้จะไม่จริง 100% แต่คุณจะเห็นได้ว่านิพจน์ Lambda แม้จะไม่ได้ปิดอยู่ แต่ก็มีความสามารถที่คล้ายกัน ในภาษาเชิงฟังก์ชัน นิพจน์แลมบ์ดาคือฟังก์ชัน แต่ใน Java นิพจน์แลมบ์ดาจะแสดงด้วยอ็อบเจ็กต์ และต้องเชื่อมโยงกับประเภทอ็อบเจ็กต์เฉพาะที่เรียกว่าอินเทอร์เฟซการทำงาน ต่อไปเรามาดูกันว่ามันคืออะไร บทความของ Mario Fusco เรื่อง "เหตุใดเราจึงต้องใช้ Lambda Expression ใน Java" อธิบายรายละเอียดว่าเหตุใดภาษาสมัยใหม่ทั้งหมดจึงจำเป็นต้องมีความสามารถในการปิดข้อมูลเบื้องต้นเกี่ยวกับนิพจน์แลมบ์ดา
นิพจน์แลมบ์ดาเป็นฟังก์ชันที่ไม่ระบุชื่อ (อาจไม่ใช่คำจำกัดความที่ถูกต้อง 100% สำหรับ Java แต่จะทำให้เกิดความชัดเจน) พูดง่ายๆ ก็คือ นี่เป็นวิธีการที่ไม่มีการประกาศ เช่น โดยไม่มีตัวดัดแปลงการเข้าถึง ส่งคืนค่าและชื่อ กล่าวโดยย่อคืออนุญาตให้คุณเขียนวิธีการและใช้งานได้ทันที มันมีประโยชน์อย่างยิ่งในกรณีของการเรียกเมธอดแบบครั้งเดียวเพราะว่า ลดเวลาในการประกาศและเขียนเมธอดโดยไม่ต้องสร้างคลาส นิพจน์ Lambda ใน Java โดยทั่วไปจะมีไวยากรณ์ดังต่อไป(аргументы) -> (тело)
นี้ ตัวอย่างเช่น:
(арг1, арг2...) -> { тело }
(тип1 арг1, тип2 арг2...) -> { тело }
ด้านล่างนี้คือตัวอย่างบางส่วนของนิพจน์ Lambda จริง:
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415 };
โครงสร้างของนิพจน์แลมบ์ดา
เรามาศึกษาโครงสร้างของนิพจน์แลมบ์ดากัน:- นิพจน์ Lambda สามารถมีพารามิเตอร์อินพุตได้ตั้งแต่ 0 ตัวขึ้นไป
- ประเภทของพารามิเตอร์สามารถระบุได้อย่างชัดเจนหรือได้รับจากบริบท เช่น (
int a
) สามารถเขียนได้ดังนี้ (a
) - พารามิเตอร์อยู่ในวงเล็บและคั่นด้วยเครื่องหมายจุลภาค ตัวอย่างเช่น (
a, b
) หรือ (int a, int b
) หรือ (String a
,int b
,float c
) - หากไม่มีพารามิเตอร์ คุณจะต้องใช้วงเล็บว่าง ตัวอย่างเช่น
() -> 42
- เมื่อมีพารามิเตอร์เพียงตัวเดียว หากไม่ได้ระบุประเภทอย่างชัดเจน ก็สามารถละเว้นวงเล็บได้ ตัวอย่าง:
a -> return a*a
- เนื้อความของนิพจน์ Lambda สามารถมีได้ 0 นิพจน์ขึ้นไป
- หากเนื้อหาประกอบด้วยคำสั่งเดียว คำสั่งนั้นอาจไม่อยู่ในวงเล็บปีกกา และอาจระบุค่าที่ส่งคืนโดยไม่มี
return
คีย์เวิร์ด - มิฉะนั้น จำเป็นต้องใช้เครื่องหมายปีกกา (บล็อกของโค้ด) และต้องระบุค่าที่ส่งคืนที่ส่วนท้ายโดยใช้คำหลัก
return
(ไม่เช่นนั้นประเภทที่ส่งคืนจะเป็นvoid
)
อินเทอร์เฟซการทำงานคืออะไร
ใน Java อินเทอร์เฟซ Marker คืออินเทอร์เฟซโดยไม่ต้องประกาศวิธีการหรือฟิลด์ กล่าวอีกนัยหนึ่ง อินเทอร์เฟซโทเค็นเป็นอินเทอร์เฟซว่างเปล่า ในทำนองเดียวกัน Functional Interfaces คืออินเทอร์เฟซที่มีวิธีนามธรรมเพียงวิธีเดียวที่ประกาศไว้java.lang.Runnable
เป็นตัวอย่างของอินเทอร์เฟซการทำงาน ประกาศเพียงวิธีเดียวvoid run()
เท่านั้น นอกจากนี้ยังมีอินเทอร์เฟซActionListener
- ยังใช้งานได้อีกด้วย ก่อนหน้านี้ เราต้องใช้คลาสที่ไม่ระบุตัวตนเพื่อสร้างออบเจ็กต์ที่ใช้อินเทอร์เฟซที่ใช้งานได้ ด้วยนิพจน์ Lambda ทุกอย่างจะง่ายขึ้น แต่ละนิพจน์แลมบ์ดาสามารถเชื่อมโยงกับอินเทอร์เฟซการทำงานบางอย่างโดยปริยายได้ ตัวอย่างเช่น คุณสามารถสร้างการอ้างอิงไปยังRunnable
อินเทอร์เฟซได้ ดังที่แสดงในตัวอย่างต่อไปนี้:
Runnable r = () -> System.out.println("hello world");
การแปลงประเภทนี้จะทำโดยปริยายเสมอเมื่อเราไม่ได้ระบุอินเทอร์เฟซที่ใช้งานได้:
new Thread(
() -> System.out.println("hello world")
).start();
ในตัวอย่างข้างต้น คอมไพลเลอร์จะสร้างนิพจน์แลมบ์ดาโดยอัตโนมัติซึ่งเป็นการนำ อินเทอร์เฟ ซRunnable
จากตัวสร้างคลาสThread
: public Thread(Runnable r) { }
นี่คือตัวอย่างบางส่วนของนิพจน์แลมบ์ดาและอินเทอร์เฟซการทำงานที่เกี่ยวข้อง:
Consumer<Integer> c = (int x) -> { System.out.println(x) };
BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);
Predicate<String> p = (String s) -> { s == null };
คำอธิบาย@FunctionalInterface
ประกอบที่เพิ่มใน Java 8 ตามข้อกำหนดภาษา Java จะตรวจสอบว่าอินเทอร์เฟซที่ประกาศนั้นใช้งานได้หรือไม่ นอกจากนี้ Java 8 ยังมีอินเทอร์เฟซการทำงานสำเร็จรูปจำนวนหนึ่งสำหรับใช้กับนิพจน์ Lambda @FunctionalInterface
จะทำให้เกิดข้อผิดพลาดในการคอมไพล์หากอินเทอร์เฟซที่ประกาศไม่ทำงาน ต่อไปนี้เป็นตัวอย่างของการกำหนดอินเทอร์เฟซการทำงาน:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
ตามคำจำกัดความที่แนะนำ ส่วนต่อประสานการทำงานสามารถมีวิธีการเชิงนามธรรมได้เพียงวิธีเดียวเท่านั้น หากคุณพยายามเพิ่มวิธีนามธรรมอื่น คุณจะได้รับข้อผิดพลาดในการคอมไพล์ ตัวอย่าง:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
public void doSomeMoreWork();
}
ข้อผิดพลาด
Unexpected @FunctionalInterface annotation @FunctionalInterface ^ WorkerInterface is not a functional interface multiple non-overriding abstract methods found in interface WorkerInterface 1 error После определения функционального интерфейса, мы можем его использовать и получать все преимущества Lambda-выражений. Пример:
// defining a functional interface @FunctionalInterface public interface WorkerInterface { public void doSomeWork(); }
public class WorkerInterfaceTest {
public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}
public static void main(String [] args) {
// calling the doSomeWork method via an anonymous class
// (classic)
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker called via an anonymous class");
}
});
// calling the doSomeWork method via Lambda expressions
// (Java 8 new)
execute( () -> System.out.println("Worker called via Lambda") );
}
}
บทสรุป:
Worker вызван через анонимный класс
Worker вызван через Lambda
ที่นี่เราได้กำหนดอินเทอร์เฟซการทำงานของเราเองและใช้นิพจน์แลมบ์ดา วิธีการนี้execute()
สามารถยอมรับนิพจน์แลมบ์ดาเป็นอาร์กิวเมนต์ได้
ตัวอย่างนิพจน์แลมบ์ดา
วิธีที่ดีที่สุดในการทำความเข้าใจนิพจน์ Lambda คือการดูตัวอย่างบางส่วน: สตรีมThread
สามารถเริ่มต้นได้สองวิธี:
// Old way:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
// New way:
new Thread(
() -> System.out.println("Hello from thread")
).start();
การจัดการเหตุการณ์ใน Java 8 สามารถทำได้ผ่านนิพจน์ Lambda ต่อไปนี้เป็นสองวิธีในการเพิ่มตัวจัดการเหตุการณ์ActionListener
ให้กับองค์ประกอบ UI:
// Old way:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button pressed. Old way!");
}
});
// New way:
button.addActionListener( (e) -> {
System.out.println("Button pressed. Lambda!");
});
ตัวอย่างง่ายๆ ของการแสดงองค์ประกอบทั้งหมดของอาร์เรย์ที่กำหนด โปรดทราบว่ามีวิธีใช้นิพจน์แลมบ์ดาได้มากกว่าหนึ่งวิธี ด้านล่างนี้เราสร้างนิพจน์แลมบ์ดาด้วยวิธีปกติโดยใช้ไวยากรณ์ลูกศร และเรายังใช้ตัวดำเนินการโคลอนคู่(::)
ซึ่งใน Java 8 จะแปลงวิธีปกติให้เป็นนิพจน์แลมบ์ดา:
// Old way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
System.out.println(n);
}
// New way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
// New way using double colon operator ::
list.forEach(System.out::println);
ในตัวอย่างต่อไปนี้ เราใช้อินเทอร์เฟซการทำงานPredicate
เพื่อสร้างการทดสอบและพิมพ์รายการที่ผ่านการทดสอบนั้น วิธีนี้ทำให้คุณสามารถใส่ตรรกะลงในนิพจน์แลมบ์ดาและทำสิ่งต่างๆ ตามนิพจน์นั้นได้
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Main {
public static void main(String [] a) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
System.out.print("Outputs all numbers: ");
evaluate(list, (n)->true);
System.out.print("Does not output any number: ");
evaluate(list, (n)->false);
System.out.print("Output even numbers: ");
evaluate(list, (n)-> n%2 == 0 );
System.out.print("Output odd numbers: ");
evaluate(list, (n)-> n%2 == 1 );
System.out.print("Output numbers greater than 5: ");
evaluate(list, (n)-> n > 5 );
}
public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.print(n + " ");
}
}
System.out.println();
}
}
บทสรุป:
Выводит все числа: 1 2 3 4 5 6 7
Не выводит ни одного числа:
Вывод четных чисел: 2 4 6
Вывод нечетных чисел: 1 3 5 7
Вывод чисел больше 5: 6 7
ด้วยการแก้ไขนิพจน์ Lambda คุณสามารถแสดงกำลังสองของแต่ละองค์ประกอบของรายการได้ โปรดสังเกตว่าเรากำลังใช้วิธีstream()
การแปลงรายการปกติให้เป็นสตรีม Java 8 มีคลาสที่ยอดเยี่ยมStream
( java.util.stream.Stream
) มีวิธีการที่เป็นประโยชน์มากมายที่คุณสามารถใช้นิพจน์แลมบ์ดาได้ เราส่งนิพจน์แลมบ์ดาx -> x*x
ไปยัง เมธอด map()
ซึ่งนำไปใช้กับองค์ประกอบทั้งหมดในสตรีม หลังจากนั้นเราจะใช้forEach
ในการพิมพ์องค์ประกอบทั้งหมดของรายการ
// Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
int x = n * n;
System.out.println(x);
}
// New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
จากรายการ คุณจะต้องพิมพ์ผลรวมของกำลังสองขององค์ประกอบทั้งหมดของรายการ นิพจน์ Lambda ช่วยให้คุณบรรลุเป้าหมายนี้ได้ด้วยการเขียนโค้ดเพียงบรรทัดเดียว ตัวอย่างนี้ใช้วิธีการบิด (ลดreduce()
) เราใช้วิธีmap()
การยกกำลังสองให้กับแต่ละองค์ประกอบ จากนั้นใช้วิธีreduce()
การยุบองค์ประกอบทั้งหมดให้เป็นตัวเลขตัวเดียว
// Old way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = 0;
for(Integer n : list) {
int x = n * n;
sum = sum + x;
}
System.out.println(sum);
// New way:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get();
System.out.println(sum);
ความแตกต่างระหว่างนิพจน์ Lambda และคลาสที่ไม่ระบุชื่อ
ข้อแตกต่างที่สำคัญคือการใช้คำthis
หลัก สำหรับคลาสที่ไม่ระบุชื่อ คีย์เวิร์ด ' ' this
หมายถึงอ็อบเจ็กต์ของคลาสที่ไม่ระบุชื่อ ในขณะที่นิพจน์ lambda ' this
' หมายถึงอ็อบเจ็กต์ของคลาสที่ใช้นิพจน์ lambda ข้อแตกต่างก็คือวิธีการรวบรวม Java รวบรวมนิพจน์แลมบ์ดาและแปลงเป็นprivate
วิธีการเรียน สิ่งนี้ใช้ คำสั่ง invokedynamicซึ่งนำมาใช้ใน Java 7 สำหรับการเชื่อมโยงเมธอดแบบไดนามิก Tal Weiss อธิบายไว้ในบล็อกของเขาว่า Java รวบรวมนิพจน์แลมบ์ดาเป็นไบต์โค้ดได้อย่างไร
GO TO FULL VERSION