JavaRush /مدونة جافا /Random-AR /Lambdas ومراجع الأساليب في ArrayList.forEach - كيف تعمل

Lambdas ومراجع الأساليب في ArrayList.forEach - كيف تعمل

نشرت في المجموعة
تبدأ مقدمة تعبيرات lambda في مهمة Java Syntax Zero بمثال محدد جدًا:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
يقوم مؤلفو المحاضرة بتحليل لامدا ومراجع الطريقة باستخدام وظيفة forEach القياسية لفئة ArrayList. شخصيا، وجدت صعوبة في فهم معنى ما كان يحدث، لأن تنفيذ هذه الوظيفة، وكذلك الواجهة المرتبطة بها، تظل "تحت الغطاء". من أين تأتي الوسيطة (الحجج) ، ومن أين يتم تمرير الدالة println()، هي أسئلة سيتعين علينا الإجابة عليها بأنفسنا. لحسن الحظ، مع IntelliJ IDEA، يمكننا بسهولة النظر في الأجزاء الداخلية لفئة ArrayList وتفكيك هذه المعكرونة من البداية. إذا كنت أيضًا لا تفهم شيئًا وتريد معرفة ذلك، فسأحاول مساعدتك في ذلك على الأقل. تعبير Lambda وArrayList.forEach - كيف يعمل من المحاضرة، نعلم بالفعل أن تعبير Lambda هو تطبيق لواجهة وظيفية . أي أننا نعلن عن واجهة ذات دالة واحدة، ونستخدم لامدا لوصف ما تفعله هذه الوظيفة. للقيام بذلك تحتاج إلى: 1. إنشاء واجهة وظيفية. 2. قم بإنشاء متغير يتوافق نوعه مع الواجهة الوظيفية؛ 3. قم بتعيين هذا المتغير تعبير لامدا الذي يصف تنفيذ الوظيفة؛ 4. استدعاء دالة عن طريق الوصول إلى متغير (ربما أكون فظًا في المصطلحات، ولكن هذه هي الطريقة الأوضح). سأقدم مثالا بسيطا من جوجل، مع تزويده بتعليقات مفصلة (شكرا لمؤلفي موقع 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 . لنحرك المؤشر فوق كلمة " المستهلك" ونضغط على المجموعة السحرية Ctrl+LMB مرة أخرى . سيتم فتح وصف لواجهة المستهلك . إذا قمنا بإزالة التنفيذ الافتراضي منه (وهو ليس مهما بالنسبة لنا الآن)، فسنرى الكود التالي:
public interface Consumer<t> {
   void accept(T t);
}
لذا. لدينا واجهة مستخدم مع وظيفة قبول واحدة تقبل وسيطة واحدة من أي نوع. نظرًا لوجود وظيفة واحدة فقط، تكون الواجهة وظيفية ويمكن كتابة تنفيذها من خلال تعبير لامدا. لقد رأينا بالفعل أن ArrayList يحتوي على دالة forEach التي تأخذ تنفيذ واجهة المستهلك كوسيطة إجراء . بالإضافة إلى ذلك، في الدالة forEach نجد الكود التالي:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
تتكرر حلقة for بشكل أساسي عبر جميع عناصر ArrayList. داخل الحلقة نرى استدعاء لوظيفة القبول لكائن الإجراء - هل تتذكر كيف قمنا بتسمية Operation.calculate؟ يتم تمرير العنصر الحالي للمجموعة إلى وظيفة القبول . الآن يمكننا أخيرًا العودة إلى تعبير لامدا الأصلي وفهم ما يفعله. دعونا نجمع كل الكود في كومة واحدة:
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) );
إن تعبير لامدا الخاص بنا هو تطبيق لوظيفة القبول الموضحة في واجهة المستهلك . باستخدام لامدا، حددنا أن وظيفة القبول تأخذ وسيطة وتعرضها على الشاشة. تم تمرير تعبير lambda إلى الدالة forEach كوسيطة الإجراء الخاصة بها ، والتي تخزن تنفيذ واجهة المستهلك . الآن يمكن لوظيفة forEach استدعاء تطبيقنا لواجهة المستهلك بسطر مثل هذا:
action.accept(elementAt(es, i));
وبالتالي، فإن وسيطة الإدخال في تعبير lambda هي عنصر آخر من مجموعة 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 ! هذا يعني أنه يمكن استخدام الأخير بنجاح كمرجع لطريقة ما - أي أن println يصبح تطبيقًا محددًا لوظيفة القبول :
list.forEach( System.out::println );
قمنا بتمرير الدالة println الخاصة بالكائن System.out كوسيطة للدالة forEach . المبدأ هو نفسه كما هو الحال مع lambda: الآن يمكن لـ forEach تمرير عنصر مجموعة إلى وظيفة println عبر استدعاء action.accept(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();
    }
آمل أن أكون قد أوضحت الموقف قليلاً على الأقل لأولئك الجدد في استخدام lambdas ومراجع الطريقة. في الختام، أوصي بالكتاب الشهير "Java: دليل المبتدئين" لروبرت شيلدت - في رأيي، تم وصف Lambdas ومراجع الوظائف بشكل معقول للغاية.
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION