การแนะนำนิพจน์แลมบ์ดาในภารกิจ Java Syntax Zero เริ่มต้นด้วยตัวอย่างที่เฉพาะเจาะจงมาก:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");
list.forEach( (s) -> System.out.println(s) );
ผู้เขียนบรรยายแยกแลมบ์ดาและการอ้างอิงวิธีการโดยใช้มาตรฐานสำหรับแต่ละฟังก์ชันของคลาส ArrayList โดยส่วนตัวแล้วฉันพบว่ามันยากที่จะเข้าใจความหมายของสิ่งที่เกิดขึ้น เนื่องจากการใช้งานฟังก์ชันนี้ตลอดจนอินเทอร์เฟซที่เกี่ยวข้องนั้นยังคง "อยู่ภายใต้ฝากระโปรง" อาร์กิวเมนต์มาจาก ไหน โดยที่ ฟังก์ชัน println() ถูกส่งผ่าน คือคำถามที่เราจะต้องตอบด้วยตัวเอง โชคดีที่ด้วย IntelliJ IDEA เราสามารถตรวจสอบภายในของคลาส ArrayList ได้อย่างง่ายดายและผ่อนคลายเส้นนี้ตั้งแต่เริ่มต้น หากคุณไม่เข้าใจสิ่งใดและต้องการทราบ ฉันจะพยายามช่วยคุณในเรื่องนี้อย่างน้อยก็นิดหน่อย นิพจน์ Lambda และ ArrayList.forEach - วิธีการทำงาน จากการบรรยาย เรารู้แล้วว่านิพจน์ lambda เป็นการนำอินเทอร์เฟซการทำงานไปใช้ นั่นคือ เราประกาศอินเทอร์เฟซด้วยฟังก์ชันเดียว และใช้แลมบ์ดาเพื่ออธิบายว่าฟังก์ชันนี้ทำอะไร ในการทำเช่นนี้คุณต้อง: 1. สร้างอินเทอร์เฟซที่ใช้งานได้; 2. สร้างตัวแปรที่มีประเภทสอดคล้องกับอินเทอร์เฟซการทำงาน 3. กำหนดนิพจน์แลมบ์ดาให้ตัวแปรนี้ซึ่งอธิบายการใช้งานฟังก์ชัน 4. เรียกใช้ฟังก์ชันโดยการเข้าถึงตัวแปร (บางทีฉันอาจใช้คำศัพท์หยาบคาย แต่นี่เป็นวิธีที่ชัดเจนที่สุด) ฉันจะยกตัวอย่างง่ายๆ จาก Google โดยให้ความคิดเห็นโดยละเอียด (ขอบคุณผู้เขียนเว็บไซต์ metanit.com):
interface Operationable {
int calculate(int x, int y);
// Единственная функция в интерфейсе — значит, это функциональный интерфейс,
// который можно реализовать с помощью лямбды
}
public class LambdaApp {
public static void main(String[] args) {
// Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
Operationable operation;
// Прописываем реализацию функции calculate с помощью лямбды, на вход подаём x и y, на выходе возвращаем их сумму
operation = (x,y)->x+y;
// Теперь мы можем обратиться к функции calculate через переменную operation
int result = operation.calculate(10, 20);
System.out.println(result); //30
}
}
ทีนี้ลองกลับมาดูตัวอย่างจากการบรรยายอีกครั้ง มีการเพิ่มองค์ประกอบหลายประเภทประเภท String ลงใน คอลเลกชัน รายการ จากนั้นองค์ประกอบต่างๆ จะถูกดึงออกมาโดยใช้ ฟังก์ชัน forEach มาตรฐาน ซึ่งถูกเรียกบน ออบเจ็ กต์รายการ นิพจน์แลมบ์ดาที่มีพารามิเตอร์แปลก ๆ บางตัวถูก ส่งผ่านเป็นอาร์กิวเมนต์ของ ฟังก์ชัน
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");
list.forEach( (s) -> System.out.println(s) );
หากคุณไม่เข้าใจทันทีว่าเกิดอะไรขึ้นที่นี่ แสดงว่าคุณไม่ได้อยู่คนเดียว โชคดีที่ IntelliJ IDEA มีแป้นพิมพ์ลัดที่ยอดเยี่ยม: Ctrl +Left_Mouse_Button หากเราวางเมาส์เหนือ forEach แล้วคลิกชุดค่าผสมนี้ ซอร์สโค้ดของคลาส ArrayList มาตรฐานจะเปิดขึ้น ซึ่งเราจะเห็นการใช้งานของเมธอดforEach :
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
final Object[] es = elementData;
final int size = this.size;
for (int i = 0; modCount == expectedModCount && i < size; i++)
action.accept(elementAt(es, i));
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
เราเห็น ว่า อาร์กิวเมนต์อินพุตคือการกระทำประเภทConsumer ลองเลื่อนเคอร์เซอร์ไปที่คำว่าConsumerแล้วกดชุดค่าผสมCtrl+LMB อีก ครั้ง คำอธิบายของ อินเทอร์เฟซ ผู้บริโภค จะเปิด ขึ้น หากเราลบการใช้งานเริ่มต้นออก (ซึ่งตอนนี้ไม่สำคัญสำหรับเรา) เราจะเห็นโค้ดต่อไปนี้:
public interface Consumer<t> {
void accept(T t);
}
ดังนั้น. เรามี อินเทอร์เฟซ ผู้บริโภค ที่มีฟังก์ชัน ยอมรับเดียวที่ยอมรับอาร์กิวเมนต์ประเภทใดก็ได้ เนื่องจากมีเพียงฟังก์ชันเดียว อินเทอร์เฟซจึงใช้งานได้ และการนำไปปฏิบัติสามารถเขียนผ่านนิพจน์แลมบ์ดาได้ เราได้เห็นแล้วว่า ArrayList มี ฟังก์ชัน forEachที่ใช้อินเท อร์เฟซ Consumerเป็น อาร์กิวเมนต์ การดำเนินการ นอกจากนี้ ในฟังก์ชัน forEach เราพบโค้ดต่อไปนี้:
for (int i = 0; modCount == expectedModCount && i < size; i++)
action.accept(elementAt(es, i));
for loop เป็นหลักจะวนซ้ำองค์ประกอบทั้งหมดของ ArrayList ภายในลูปเราเห็นการเรียกไปยัง ฟังก์ชัน ยอมรับ ของ วัตถุการกระทำ - จำวิธีที่เราเรียกว่าการดำเนินการคำนวณ? องค์ประกอบปัจจุบันของคอลเลก ชัน จะถูกส่ง ไปยัง ฟังก์ชัน ยอมรับ ในที่สุดเราก็สามารถกลับไปใช้นิพจน์แลมบ์ดาดั้งเดิมได้แล้ว และเข้าใจว่ามันทำอะไร มารวบรวมโค้ดทั้งหมดไว้ในกองเดียว:
public interface Consumer<t> {
void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}
public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована нашей лямбдой {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
final Object[] es = elementData;
final int size = this.size;
for (int i = 0; modCount == expectedModCount && i < size; i++)
action.accept(elementAt(es, i)); // Вызываем нашу реализацию функции accept интерфейса Consumer для каждого element коллекции
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
//...
list.forEach( (s) -> System.out.println(s) );
นิพจน์แลมบ์ดาของเราคือการนำ ฟังก์ชัน ยอมรับที่อธิบายไว้ใน อินเทอร์เฟซ ผู้บริโภคไป ใช้ เมื่อใช้แลมบ์ดา เราระบุว่า ฟังก์ชัน ยอมรับรับอาร์กิวเมนต์sและแสดงบนหน้าจอ นิพจน์ lambda ถูกส่งไปยัง ฟังก์ชัน forEachเป็น อาร์กิวเมนต์ actionซึ่งจัดเก็บการใช้งาน อินเทอร์เฟ ซผู้บริโภค ตอนนี้ฟังก์ชัน forEach สามารถเรียกการใช้งานอินเทอร์เฟซ Consumer ของเราได้ด้วยบรรทัดดังนี้:
action.accept(elementAt(es, i));
ดังนั้น อาร์กิวเมนต์อินพุตsในนิพจน์แลมบ์ดาจึงเป็นองค์ประกอบถัดไปของคอลเลกชัน ArrayListซึ่งถูกส่งไปยังการใช้งานอินเทอร์เฟซผู้บริโภค ของ เรา ทั้งหมดนี้: เราได้วิเคราะห์ตรรกะของนิพจน์แลมบ์ดาใน ArrayList.forEach แล้ว การอ้างอิงถึงวิธีการใน ArrayList.forEach - มันทำงานอย่างไร ขั้นตอนต่อไปในการบรรยายคือการดูการอ้างอิงวิธีการ จริงอยู่ที่พวกเขาเข้าใจมันด้วยวิธีที่แปลกมาก - หลังจากอ่านการบรรยายแล้วฉันไม่มีโอกาสเข้าใจว่าโค้ดนี้ทำอะไร:
list.forEach( System.out::println );
ก่อนอื่น ทฤษฎีเล็กๆ น้อยๆ อีกครั้ง การอ้างอิงวิธีการคือการนำอินเทอร์เฟซการทำงานที่อธิบายโดยฟังก์ชันอื่นไปใช้อย่าง คร่าว ๆ ฉันจะเริ่มต้นด้วยตัวอย่างง่ายๆ อีกครั้ง:
public interface Operationable {
int calculate(int x, int y);
// Единственная функция в интерфейсе — значит, это функциональный интерфейс
}
public static class Calculator {
// Создадим статический класс Calculator и пропишем в нём метод methodReference.
// Именно он будет реализовывать функцию calculate из интерфейса Operationable.
public static int methodReference(int x, int y) {
return x+y;
}
}
public static void main(String[] args) {
// Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
Operationable operation;
// Теперь реализацией интерфейса будет не лямбда-выражение, а метод methodReference из нашего класса Calculator
operation = Calculator::methodReference;
// Теперь мы можем обратиться к функции интерфейса через переменную operation
int result = operation.calculate(10, 20);
System.out.println(result); //30
}
กลับมาที่ตัวอย่างจากการบรรยาย:
list.forEach( System.out::println );
ฉันขอเตือนคุณว่าSystem.outเป็นวัตถุประเภท PrintStream ที่มีฟังก์ชันprintln วางเมาส์เหนือprintlnแล้วคลิกCtrl+LMB :
public void println(String x) {
if (getClass() == PrintStream.class) {
writeln(String.valueOf(x));
} else {
synchronized (this) {
print(x);
newLine();
}
}
}
มาดูคุณสมบัติหลักสองประการกัน: 1. ฟังก์ชัน printlnจะไม่ส่งคืนสิ่งใดๆ (เป็นโมฆะ) 2. ฟังก์ชัน printlnรับหนึ่งอาร์กิวเมนต์เป็นอินพุต ไม่เตือนคุณถึงอะไรเลยเหรอ?
public interface Consumer<t> {
void accept(T t);
}
ถูกต้อง - ลายเซ็นฟังก์ชัน ยอมรับเป็นกรณีทั่วไปของ เมธอด println Signature ! ซึ่งหมายความว่าวิธีหลังสามารถใช้เป็นข้อมูลอ้างอิงถึงเมธอดได้สำเร็จ นั่นคือprintln กลายเป็นการใช้งานเฉพาะของฟังก์ชัน Accept :
list.forEach( System.out::println );
เราส่ง ฟังก์ชัน printlnของวัตถุSystem.outเป็นอาร์กิวเมนต์ไปยังฟังก์ชันforEach หลักการเหมือนกับ lambda: ตอนนี้ forEach สามารถส่งผ่านองค์ประกอบคอลเลกชันไปยังฟังก์ชัน println ผ่านการ เรียก action.accept(elementAt(es, i) ) System.out.println(elementAt(es, i))ที่จริงแล้วตอนนี้สามารถอ่านได้เป็นชื่อ System.out.println(elementAt(es, i) )
public void forEach(Consumer<? super E> action) // В action хранится an object Consumer, в котором функция accept реализована методом println {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
final Object[] es = elementData;
final int size = this.size;
for (int i = 0; modCount == expectedModCount && i < size; i++)
action.accept(elementAt(es, i)); // Функция accept теперь реализована методом System.out.println!
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
ฉันหวังว่าฉันจะได้ชี้แจงสถานการณ์อย่างน้อยเล็กน้อยสำหรับผู้ที่ยังใหม่กับแลมบ์ดาและการอ้างอิงวิธีการ โดยสรุป ฉันขอแนะนำหนังสือชื่อดัง "Java: A Beginner's Guide" โดย Robert Schildt - ในความคิดของฉัน การอ้างอิงแลมบ์ดาและฟังก์ชันมีการอธิบายไว้ค่อนข้างสมเหตุสมผล
GO TO FULL VERSION