JavaRush /Java Blog /Random-TW /ArrayList.forEach 中的 Lambda 和方法引用 - 工作原理

ArrayList.forEach 中的 Lambda 和方法引用 - 工作原理

在 Random-TW 群組發布
Java 語法零探索中對 lambda 表達式的介紹從一個非常具體的範例開始:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hello", "How", "дела?");

list.forEach( (s) -> System.out.println(s) );
本講座的作者使用 ArrayList 類別的標準 forEach 函數來解析 lambda 和方法引用。就我個人而言,我發現很難理解正在發生的事情的含義,因為該函數的實現以及與之相關的介面仍然處於「幕後」。參數從哪裡來,println()函數在哪裡傳遞,這些都是我們必須自己回答的問題。幸運的是,借助 IntelliJ IDEA,我們可以輕鬆地了解 ArrayList 類別的內部結構,並從頭開始解開這個難題。如果你還有什麼不明白並且想弄清楚,我會盡力幫助你,至少一點點。 Lambda 表達式和 ArrayList.forEach - 工作原理 從講座中我們已經知道lambda 表達式是函數式介面的實作。也就是說,我們用一個函數宣告一個接口,並使用 lambda 來描述該函數的作用。為此,您需要: 1. 建立一個功能介面;2、建立一個類型與函數介面對應的變數;3. 為該變數指派一個描述函數實現的 lambda 表達式;4. 透過存取變數來呼叫函數(也許我的術語很粗糙,但這是最清晰的方法)。我將給出一個來自 Google 的簡單範例,並提供詳細的註釋(感謝網站 metait.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的 lambda 表達式作為參數傳遞給函數。
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接口,它帶有一個接受函數,可以接受任何類型的一個參數。既然只有一個函數,那麼介面就是函數式的,其實作可以透過lambda表達式來寫。我們已經看到 ArrayList 有一個forEach函數,它將Consumer介面的實作作為操作參數。另外,在forEach函數中我們發現如下程式碼:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
for 迴圈本質上是遍歷 ArrayList 的所有元素。在循環內部,我們看到對操作對象的接受函數的呼叫- 還記得我們如何呼叫 operation.calculate 嗎?集合的目前元素被傳遞給accept函數。現在我們終於可以回到最初的 lambda 表達式並了解它的作用了。讓我們將所有程式碼收集到一堆:
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 表達式是Consumer介面中所描述的接受函數 的實作。使用 lambda,我們指定接受函數接受參數s並將其顯示在螢幕上。lambda 表達式作為其操作參數傳遞給forEach函數,該函數儲存Consumer介面的實作。現在 forEach 函數可以使用以下行呼叫 Consumer 介面的實作:
action.accept(elementAt(es, i));
因此, lambda 表達式中的 輸入參數s是ArrayList 集合的下一個元素,該元素傳遞給Consumer 介面的實作。就這樣:我們已經分析了 ArrayList.forEach 中 lambda 表達式的邏輯。 引用 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是一個具有println函數的 PrintStream 類型的物件。讓我們將滑鼠懸停在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 );
我們將System.out物件的println函數作為參數傳遞給forEach函數。原理與 lambda 相同:現在 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();
    }
我希望我至少已經為那些剛接觸 lambda 和方法引用的人澄清了一些情況。總之,我推薦 Robert Schildt 的名著《Java:初學者指南》——在我看來,其中對 lambda 表達式和函數引用的描述相當明智。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION