JavaRush /وبلاگ جاوا /Random-FA /لامبداها و مراجع متد در ArrayList.forEach - چگونه کار می ...

لامبداها و مراجع متد در ArrayList.forEach - چگونه کار می کند

در گروه منتشر شد
مقدمه عبارات لامبدا در جستجوی Java Syntax Zero با یک مثال بسیار خاص آغاز می شود:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
نویسندگان سخنرانی، لامبداها و مراجع روش را با استفاده از تابع استاندارد forEach از کلاس ArrayList تجزیه می کنند. شخصاً درک معنای آنچه در حال رخ دادن بود دشوار بود ، زیرا اجرای این عملکرد و همچنین رابط مرتبط با آن "زیر هود" باقی می ماند. جایی که آرگومان (s) از کجا می آید ، از کجا تابع println() ارسال می شود ، سوالاتی هستند که خودمان باید به آنها پاسخ دهیم. خوشبختانه، با IntelliJ IDEA، می‌توانیم به راحتی به درونی‌های کلاس ArrayList نگاه کنیم و از همان ابتدا این نودل را باز کنیم. اگر شما هم چیزی نمی‌فهمید و می‌خواهید آن را بفهمید، من سعی می‌کنم حداقل در این مورد به شما کمک کنم. عبارت Lambda و ArrayList.forEach - چگونه کار می کند از سخنرانی قبلاً می دانیم که عبارت lambda پیاده سازی یک رابط کاربردی است . یعنی ما یک رابط را با یک تابع واحد اعلام می کنیم و از یک لامبدا برای توصیف عملکرد این تابع استفاده می کنیم. برای انجام این کار شما نیاز دارید: 1. ایجاد یک رابط کاربردی. 2. یک متغیر ایجاد کنید که نوع آن با رابط عملکردی مطابقت دارد. 3. به این متغیر یک عبارت lambda اختصاص دهید که اجرای تابع را توصیف می کند. 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 که در شی لیست فراخوانی می شود، بازیابی می شوند . یک عبارت لامبدا با چند پارامتر عجیب s به عنوان آرگومان به تابع ارسال می شود .
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 دارد که پیاده‌سازی رابط مصرف‌کننده را به‌عنوان آرگومان عمل می‌گیرد . علاوه بر این، در تابع forEach کد زیر را پیدا می کنیم:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
حلقه for اساساً در تمام عناصر یک ArrayList تکرار می شود. در داخل حلقه ما یک فراخوانی به تابع پذیرش شی اکشن می بینیم - به یاد دارید که چگونه عملیات.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 ما پیاده سازی تابع پذیرش است که در رابط مصرف کننده توضیح داده شده است . با استفاده از لامبدا، مشخص کردیم که تابع accept آرگومان s را می گیرد و آن را روی صفحه نمایش می دهد. عبارت لامبدا به تابع forEach به عنوان آرگومان عمل آن ارسال شد که اجرای رابط مصرف کننده را ذخیره می کند . اکنون تابع forEach می تواند پیاده سازی ما از رابط مصرف کننده را با خطی مانند این فراخوانی کند:
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 چیزی را بر نمی گرداند (void). 2. تابع println یک آرگومان را به عنوان ورودی دریافت می کند. شما را به یاد چیزی نمی اندازد؟
public interface Consumer<t> {
   void accept(T t);
}
درست است - امضای تابع پذیرش یک مورد کلی تر از امضای متد println است ! این بدان معنی است که روش دوم را می توان با موفقیت به عنوان مرجع یک روش استفاده کرد - یعنی println به یک پیاده سازی خاص از تابع accept تبدیل می شود :
list.forEach( System.out::println );
تابع println شی System.out را به عنوان آرگومان به تابع forEach ارسال کردیم . اصل کار مانند لامبدا است: اکنون forEach می تواند یک عنصر مجموعه را از طریق یک action.accept(elementAt(es, i)) به تابع println ارسال کند . در واقع، اکنون می توان آن را به عنوان 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();
    }
امیدوارم برای کسانی که تازه با لامبدا و مراجع متد آشنا هستند، حداقل کمی وضعیت را روشن کرده باشم. در پایان، من کتاب معروف "جاوا: راهنمای مبتدی" توسط رابرت شیلد را توصیه می کنم - به نظر من، لامبداها و مراجع تابع کاملاً معقولانه در آن توصیف شده اند.
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION