JavaRush /จาวาบล็อก /Random-TH /การอ้างอิง Lambdas และเมธอดใน ArrayList.forEach - วิธีการ...
Anonymous #2633326
ระดับ

การอ้างอิง Lambdas และเมธอดใน ArrayList.forEach - วิธีการทำงาน

เผยแพร่ในกลุ่ม
การแนะนำนิพจน์แลมบ์ดาในภารกิจ 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 - ในความคิดของฉัน การอ้างอิงแลมบ์ดาและฟังก์ชันมีการอธิบายไว้ค่อนข้างสมเหตุสมผล
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION