JavaRush /בלוג Java /Random-HE /למבדות והפניות לשיטות ב-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. באופן אישי, התקשיתי להבין את המשמעות של המתרחש, שכן יישום הפונקציה הזו, כמו גם הממשק הקשור אליה, נשאר "מתחת למכסה המנוע". מאיפה מגיע הארגומנט (ים) , היכן מועברת הפונקציה println() הן שאלות שנצטרך לענות בעצמנו. למרבה המזל, עם IntelliJ IDEA, נוכל להסתכל בקלות על החלק הפנימי של מחלקת ArrayList ולהירגע מהנודל הזה מההתחלה. אם גם אתה לא מבין כלום ורוצה להבין את זה, אנסה לעזור לך בזה לפחות קצת. Lambda expression ו-ArrayList.forEach - איך זה עובד מההרצאה אנחנו כבר יודעים שביטוי למבדה הוא יישום של ממשק פונקציונלי . כלומר, אנו מכריזים על ממשק עם פונקציה אחת בודדת, ומשתמשים בלמבדה כדי לתאר מה הפונקציה הזו עושה. לשם כך אתה צריך: 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 , הנקראת באובייקט הרשימה . ביטוי למבדה עם פרמטר מוזר 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);
}
כך. יש לנו ממשק Consumer עם פונקציית קבלה אחת שמקבלת ארגומנט אחד מכל סוג שהוא. מכיוון שיש רק פונקציה אחת, אז הממשק פונקציונלי, וניתן לכתוב את היישום שלו באמצעות ביטוי למבדה. כבר ראינו שלArrayList יש פונקציה forEach שלוקחת יישום של ממשק הצרכן כארגומנט פעולה . בנוסף, בפונקציה forEach אנו מוצאים את הקוד הבא:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
לולאת for בעצם חוזרת דרך כל האלמנטים של ArrayList. בתוך הלולאה אנו רואים קריאה לפונקציית accept של אובייקט הפעולה - זוכרים איך קראנו operation.calculate? הרכיב הנוכחי של האוסף מועבר לפונקציה accept . כעת נוכל סוף סוף לחזור לביטוי הלמבדה המקורי ולהבין מה הוא עושה. בואו נאסוף את כל הקוד בערימה אחת:
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 ומציגה אותו על המסך. ביטוי lambda הועבר לפונקציה forEach כארגומנט הפעולה שלה , המאחסן את היישום של ממשק הצרכן . כעת הפונקציה forEach יכולה לקרוא ליישום שלנו של ממשק הצרכן בשורה כזו:
action.accept(elementAt(es, i));
לפיכך, ארגומנט הקלט s בביטוי 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 לא מחזירה כלום (void). 2. הפונקציה println מקבלת ארגומנט אחד כקלט. לא מזכיר לך כלום?
public interface Consumer<t> {
   void accept(T t);
}
זה נכון - חתימת הפונקציה accept היא מקרה כללי יותר של חתימת השיטה println ! המשמעות היא שניתן להשתמש באחרון בהצלחה כהתייחסות לשיטה - כלומר, 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)) .
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" מאת רוברט שילד – לדעתי, למבדות והפניות לתפקוד מתוארים בו בצורה הגיונית למדי.
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION