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

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

已在 Random-ZH 群组中发布
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