这篇文章是为谁准备的?
该材料并没有声称具有任何学术价值,更不用说新颖性了。恰恰相反:我将尝试尽可能简单地描述(对某些人而言)复杂的事物。解释 Stream API 的请求启发了我写这篇文章。我考虑了一下,决定如果不了解 lambda 表达式,我的一些流示例将难以理解。所以我们将从 lambda 表达式开始。
- 适用于阅读本文第一部分的人;
- 它适用于那些认为自己已经很了解 Java Core 但对 Java 中的 lambda 表达式一无所知的人。或者他们可能听说过有关 lambda 表达式的一些信息,但缺少详细信息。
- 适合对lambda表达式有一定了解,但仍然望而却步,不习惯使用的人。

访问外部变量
此代码是否使用匿名类编译?int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
不,counter
变量必须是final
. 或者如果不是final
,那么至少它不能改变它的价值。同样的原则也适用于 lambda 表达式。他们可以从声明的地方访问他们可以“看到”的所有变量。但是 lambda 不得更改它们(为它们分配新值)。然而,在匿名类中有一种方法可以绕过这个限制。只需创建一个引用变量并更改对象的内部状态。这样做时,变量本身不会改变(指向同一个对象)并且可以安全地标记为final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
这里我们的counter
变量是对对象的引用AtomicInteger
。并且该incrementAndGet()
方法用于更改此对象的状态。变量本身的值在程序运行时不会改变。它总是指向同一个对象,这让我们可以用 final 关键字声明变量。以下是相同的示例,但使用了 lambda 表达式:
int counter = 0;
Runnable r = () -> counter++;
由于与具有匿名类的版本相同的原因,这不会编译: counter
在程序运行时不得更改。但如果我们这样做,一切都很好:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
这也适用于调用方法。在 lambda 表达式中,您不仅可以访问所有“可见”变量,还可以调用任何可访问的方法。
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
虽然staticMethod()
是私有的,但它可以在main()
方法内部访问,因此也可以从方法中创建的 lambda 内部调用它main
。
什么时候执行 lambda 表达式?
你可能会觉得下面的问题太简单了,但你还是应该问:lambda 表达式里面的代码什么时候执行?什么时候创建的?或者什么时候被调用(目前还不知道)?这很容易检查。System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
屏幕输出:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
您可以看到 lambda 表达式是在线程创建之后的最后执行的,并且仅在程序执行到方法时执行run()
。当然不是在宣布的时候。通过声明 lambda 表达式,我们仅创建了一个Runnable
对象并描述了其run()
方法的行为方式。该方法本身的执行时间要晚得多。
方法参考?
方法引用与 lambda 没有直接关系,但我认为在本文中对它们说几句是有意义的。假设我们有一个 lambda 表达式,它不执行任何特殊操作,只是简单地调用一个方法。x -> System.out.println(x)
它接收一些x
只是调用System.out.println()
,传入x
。在这种情况下,我们可以将其替换为对所需方法的引用。像这样:
System.out::println
没错——末尾没有括号!这是一个更完整的例子:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
在最后一行,我们使用forEach()
方法,它接受一个实现接口的对象Consumer
。同样,这是一个函数式接口,只有一个void accept(T t)
方法。因此,我们编写了一个只有一个参数的 lambda 表达式(因为它是在接口本身中键入的,所以我们不指定参数类型;我们只表示我们将调用它x
)。accept()
在 lambda 表达式的主体中,我们编写了调用方法时将执行的代码。这里我们简单地显示变量中的结果x
。这个相同的forEach()
方法遍历集合中的所有元素并调用accept()
方法的实现Consumer
接口(我们的 lambda),传入集合中的每个项目。正如我所说,我们可以用对所需方法的引用替换这样的 lambda 表达式(一个简单地对不同方法进行分类的表达式)。那么我们的代码将如下所示:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
主要是println()
和accept()
方法的参数要匹配。因为该方法可以接受任何东西(它为所有基元类型和所有对象重载),而不是 lambda 表达式,我们可以简单地将对该方法的println()
引用传递给. 然后将获取集合中的每个元素并将其直接传递给方法。对于第一次遇到这种情况的任何人,请注意我们不是在调用(单词之间有点,末尾有括号)。相反,我们正在传递对此方法的引用。如果我们这样写 println()
forEach()
forEach()
println()
System.out.println()
strings.forEach(System.out.println());
我们将有一个编译错误。在调用 之前forEach()
,Java 看到它System.out.println()
正在被调用,因此它知道返回值是void
并将尝试传递void
给forEach()
,而后者期待的是一个Consumer
对象。
方法引用的语法
这很简单:-
我们像这样传递对静态方法的引用:
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
我们使用现有对象传递对非静态方法的引用,如下所示:
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
我们使用实现它的类传递对非静态方法的引用,如下所示:
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
我们像这样传递对构造函数的引用:
ClassName::new
当您已经拥有可以完美用作回调的方法时,方法引用非常方便。在这种情况下,不是编写包含方法代码的 lambda 表达式,也不是编写仅调用该方法的 lambda 表达式,我们只是传递对它的引用。就是这样。
匿名类和 lambda 表达式之间有趣的区别
在匿名类中,this
关键字指向匿名类的一个对象。但是如果我们在 lambda 中使用它,我们就可以访问包含类的对象。我们实际编写 lambda 表达式的地方。发生这种情况是因为 lambda 表达式被编译到它们所在类的私有方法中。我不建议使用此“功能”,因为它有副作用并且与函数式编程的原则相矛盾。也就是说,这种方法完全符合 OOP。;)
我从哪里得到我的信息,你还应该阅读什么?
- Oracle官网教程。很多详细信息,包括示例。
- 同一 Oracle 教程中有关方法参考的章节。
- 如果您真的很好奇,请进入维基百科。
GO TO FULL VERSION