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 表达式和函数引用的描述相当明智。
GO TO FULL VERSION