Java 8 中的 Lambda 表达式






4.70/5 (12投票s)
本文介绍了 Java 8 的新特性——lambda 表达式的使用。
引言
在新的 Java 8 中,我们终于有了 lambda 表达式。这可能是最新版本中最重要的特性,它使得编码更快、更清晰,并为函数式编程打开了大门。下面是它的工作原理。
Java 是一种面向对象的编程语言,创建于 90 年代,当时面向对象范式是软件开发的主流。早在之前就有 Lisp 等函数式编程语言,但它们的好处在学术界之外并未得到太多认可。最近,函数式编程变得越来越重要,因为它非常适合并发和事件驱动编程。这并不意味着面向对象被边缘化或现在不好。相反,最好的策略是将这两种范式混合使用。即使您对并行编程不感兴趣,这也有用。例如,如果语言支持方便的函数式表达式语法,集合库可以拥有强大的 API。
Java 8 的主要改进是在其面向对象的基础上增加了对函数式编程的支持。在本文中,我将展示基本语法,并探讨如何在几种不同的上下文中使用它。
为什么我们需要 lambda?
lambda 表达式是一个代码块,您可以将其传递给其他地方,以便稍后执行它,一次或多次。在深入研究语法之前,让我们回顾一下您以前在开发过程中使用过类似代码结构的地方。
让我们考虑一个使用自定义比较器进行排序的示例。如果您想按长度而不是字典顺序(默认)对字符串进行排序,您应该将一个 Comparator
对象传递给 sort
方法
class LengthStringComparator implements Comparator<String> {
public int compare(String firstStr, String secondStr) {
return Integer.compare(firstStr.length(), secondStr.length());
}
}
Arrays.sort(strings, new LengthStringComparator ());
sort
方法调用 compare
方法,如果项目未排序,则重新排列项目,直到数组排序完成。您为 sort
方法提供了一个用于比较元素的代码片段,它与您不需要重新实现的其余排序逻辑集成。请注意,调用 Integer.compare(x, y)
在 x 和 y 相等时返回零,在 x < y 时返回负数,在 x > y 时返回正数。它是一个静态方法,添加到 Java 7 中。您不应该计算 x - y 来比较 x 和 y,因为对于大且符号相反的操作数,它可能会溢出。
如果您想在另一个线程中执行一些操作,您可以将这些步骤放在 Runnable
的 run
方法中,如下所示
class MyRunner implements Runnable {
public void run() {
for (int i = 0; i < 1000; i++)
doWork();
}
...
}
然后,当您想执行此代码时,您实例化一个 MyRunner
对象。然后您可以将实例放入线程池,或者简单地启动一个新线程
MyRunner r = new MyRunner();
new Thread(r).start();
关键点是 run
方法包含您想要在单独线程中执行的代码。
延迟执行的另一个例子是按钮回调。您创建一个实现侦听器接口的类,并将操作回调放入其方法中,创建类实例,并将实例注册到按钮。这种情况非常普遍,许多程序员经常使用“匿名类的匿名实例”语法
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
System.out.println("The button has been clicked!");
}
});
您应该注意到,此代码在每次单击按钮时都会执行。
在我的示例中,我使用 JavaFX,因为 Java 8 将 JavaFX 定位为 Swing GUI 工具包的继任者。细节并不重要。每个用户界面工具包,无论是 Swing 还是 JavaFX,都有相同的习惯用法——您给按钮提供一些您希望在单击按钮时运行的代码片段。
在所有三个代码示例中,您都看到了相似的想法。您将一个代码块传递给某人——一个线程池、一个排序方法或一个按钮。并且此代码在稍后某个时间被调用。
以前在 Java 中,将代码块传递给某人并不那么容易。Java 是一种面向对象的语言,因此您必须创建一个特殊的类,其中包含所需代码的方法,然后实例化一个类对象。
在其他语言(如 C#)中,您可以直接使用代码块。语言设计者多年来一直反对添加此功能。首先,Java 的一个巨大优点是其简洁性和一致性。如果一种语言包含所有选项,这些选项只能让代码稍微缩短,那么它可能会变成一堆混乱的垃圾。然而,在那些其他语言中,生成线程或绑定按钮点击处理程序不仅仅是更容易。结果是许多库更简单、更一致且更强大。在 Java 中,可以用旧式编写类似的 API,但使用起来不太方便。
在过去的几年里,问题不是是否将函数式编程元素添加到 Java 中,而是如何添加。经过一段时间的实验,才清楚这些特性适合 Java。在下一节中,您将看到如何在 Java 中使用代码块。
Lambda 的语法
让我们再次回到排序示例。我们提供了确定哪个字符串更短的代码。我们计算
Integer.compare(firstStr.length(), secondStr.length())
firstStr
和 secondStr
是什么对象?它们都是字符串!Java 是一种强类型语言,因此我们还必须指定参数类型
(String firstStr, String secondStr)
-> Integer.compare(firstStr.length(),secondStr.length())
您刚刚编写了您的第一个 lambda 表达式!此表达式指定一个代码块以及必须传递给代码的变量。
再讲一点历史……为什么叫这个名字?很久以前,当人们还没有想到电脑时,数学家阿隆佐·丘奇(Alonzo Church)想形式化数学函数可以被有效计算的含义。(奇怪的是,有些函数已知存在,但没人知道如何计算它们的值。)他使用希腊符号 lambda (λ) 来标记参数。
他为什么恰好使用那个字母?他真的用完了字母表中的所有字母吗?丘奇从数学巨著《数学原理》(Principia Mathematica)中借鉴了使用这个字母的想法,其中自由变量用 ˆ 符号标记,这启发丘奇使用大写 lambda (Λ) 表示函数的参数。然而,最终他决定使用小写版本。从那时起,带有参数变量的表达式就被称为“lambda 表达式”。
Java lambda 有几种略有不同的形式。让我们更仔细地考虑它们。您刚刚看到了其中一种:参数、-> 箭头和一个表达式。如果代码包含不适合单个表达式的计算,请像编写方法一样编写它:将代码放入 {} 并添加显式的 return
语句。例如,
(String firstStr, String secondStr) -> {
if (firstStr.length() < secondStr.length()) return -1;
else if (firstStr.length() > secondStr.length()) return 1;
else return 0;
}
如果 lambda 中没有参数,您仍然应该放置空括号,就像无参数方法一样
() -> { for (int i = 0; i < 1000; i++) doSomething(); }
如果 lambda 的参数类型可以推断,您可以省略它们。例如,
Comparator<String> comp
= (firstStr, secondStr) // Same as (String firstStr, String secondStr)
-> Integer.compare(firstStr.length(),secondStr.length());
此时,编译器可以判断 firstStr
和 secondStr
是字符串,因为我们将 lambda 分配给字符串比较器。(我们稍后会更仔细地查看此代码。)
如果方法只有一个参数,并且其类型可以由编译器推断,您甚至可以省略括号
EventHandler<ActionEvent> listener = event ->
System.out.println("The button has been clicked!");
// Instead of (event) -> or (ActionEvent event) ->
此外,您可以像处理方法参数一样将 final
修饰符和注解放置到 lambda 参数中
(final String var) -> ...
(@NonNull String var) -> ...
您永远不需要指定 lambda 表达式的结果类型。编译器总是从上下文中推断它。例如,您可以使用 lambda
(String firstStr, String secondStr) -> Integer.compare(firstStr.length(), secondStr.length())
其中预期结果类型为 int
。
请注意,在 lambda 中,您不能在所有分支中返回值。例如,(int x) -> { if (x <= 1) return -1; }
是无效的。
函数式接口
正如我们刚刚讨论的,Java 有许多现有的接口封装了代码块,例如 Runnable
或 Comparator
。Lambda 适用于这些目的。
在 Java 中有所谓的函数式接口——它是一个实现只有一个抽象方法的接口的对象。每当需要一个函数式接口的对象时,您都可以提供一个 lambda 表达式。
您可能会问,为什么函数式接口必须只有一个抽象方法。我们知道接口中的所有方法都是抽象的,不是吗?实际上,接口总是可以重新声明来自 Object
类的方法,例如 toString
或 clone
,并且这些声明不会使这些方法成为抽象方法。(Java API 中的一些接口重新声明 Object
方法是为了附加 javadoc 注释。Comparator API 就是一个例子。)更重要的是,正如您将在下面看到的,我们可以在 Java 8 的接口中声明非抽象方法。
让我们考虑 Arrays.sort
方法的例子。在这里我们可以看到函数式接口被 lambda 替代。我们只需将 lambda 作为第二个参数传递给该方法,该参数需要一个 Comparator
对象,这是一个只有一个方法的接口。
Arrays.sort(strs,
(firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length()));
实际上,Arrays.sort
方法接收一个实现了 Comparator<String>
的某个类的对象。当 compare
方法被调用时,它会强制执行 lambda 表达式的主体。这些对象的结构和类完全取决于实现。它不仅可以使用传统的内部类。也许最好将 lambda 表示为一个函数,而不是一个对象,并发现我们可以将它传递给一个函数式接口。
这种向接口的转换正是 lambda 表达式如此令人兴奋的原因。语法简洁明了。这是另一个例子
button.setOnAction(event ->
System.out.println("The button has been clicked!"));
您同意它非常易读吗?
事实上,您在 Java 中可以对 lambda 表达式做的唯一一件事就是这样的转换。在其他编程语言(例如 C#)中,您可以声明 lambda 类型,例如 (String, String) -> int
,声明这些类型的变量,并使用这些变量来保存函数表达式。在 Java 中,甚至不要尝试将 lambda 表达式分配给 Object
类型的变量,因为 Object
不是函数式接口。Java 设计者决定限制 lambda 的操作,而不是向语言添加特殊类型。
Java API 的 java.util.function 包中有几个泛型函数式接口。其中之一,BiFunction<T, U, R>
,表示一个参数类型为 T
和 U
,返回类型为 R
的函数。您可以将您的字符串比较 lambda 分配给这样的变量
BiFunction<String, String, Integer> compareFunc
= (firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length());
然而,这不适合我们的目的。没有要求 BiFunction
的 Arrays.sort
方法。这对于经验丰富的函数式语言开发人员来说可能很好奇。然而,这在 Java 中很常见。像 Comparator
这样的接口是为特定情况设计的,而不仅仅是一个带有一些参数和返回类型的方法。这是 Java 8 的风格。
您可以在不同的 Java 8 API 中看到这些来自 java.util.function 的接口。在 Java 8 中,任何函数式接口都可以用 @FunctionalInterface
标记。这个注解是可选的,但出于两个原因,它是一种好风格。首先,它强制编译器检查被注解的实体是否是一个只有一个抽象方法的接口。第二个优点是 javadoc 页面包含一个声明,说明您的接口是一个函数式接口。任何只有一个抽象方法的接口,根据定义,都是一个函数式接口。然而,使用这个关键字可以使清晰度更高。
顺便说一下,请记住在 lambda 转换为函数式接口时可能会出现受检异常。如果 lambda 表达式的主体可以抛出受检异常,您应该在目标接口的抽象方法中声明此异常。例如,以下代码将导致错误
Runnable sleepingRunner = () -> { System.out.println("…"); Thread.sleep(1000); };
// Error: Thread.sleep can throw a checkedInterruptedException
这个语句是不正确的,因为 run
方法不能抛出任何异常。有两种方法可以解决这个问题。一种方法是在 lambda 主体中捕获异常。第二种方法是将此 lambda 分配给一个只有一个抽象方法且可以抛出异常的接口。例如,接口 Callable
的 call
方法可以生成任何异常。因此,如果您在 lambda 主体的末尾添加 return null
,您可以将您的 lambda 分配给一个 Callable<Void>
实例。
方法引用
有时您已经有了一个适合您需求的方法,并且希望将其传递给其他调用。例如,假设您希望在单击按钮时简单地打印事件对象。当然,您可以这样写
button.setOnAction(event -> System.out.println(event));
对您来说,更方便的是直接将 println
方法传递给 setOnAction
方法。以下示例展示了这一点
button.setOnAction(System.out::println);
System.out::println
是一个方法引用,类似于 lambda 表达式。我们可以在这里用方法引用替换 lambda。
假设您想在不区分大小写的情况下对字符串进行排序。您可以编写如下代码
Arrays.sort(strs, String::compareToIgnoreCase)
运算符 ::
将方法名称与对象或类的名称分开。主要有三种情况
- 对象的实例方法;
- 类的静态方法;
- 类的实例方法;
在前两种情况下,方法引用等价于带有方法参数的 lambda 表达式。如上所示,System.out::println
等价于 x -> System.out.println(x)
。同样,Math::pow
等价于 (x, y) -> Math.pow(x, y)
。在第三种情况下,第一个参数成为方法的调用目标。例如,String::compareToIgnoreCase
与 (x, y) -> x.compareToIgnoreCase(y)
相同。
如您所知,可能存在多个同名重载方法。在这种情况下,编译器将尝试从上下文中找出要选择哪个方法。例如,Math.max
方法有两个版本,一个用于 int 值,一个用于 double 值。调用哪个版本取决于 Math::max
转换成的函数式接口的方法签名。方法引用不能单独存在。与 lambda 类似,它们在幕后总是被转换为函数式接口的实例。
您可能会问是否可以在方法引用中捕获参数 this
。是的,可以。例如,this::equals
等价于 x -> this.equals(x)
。也可以使用 super
。当我们使用 super::instanceMethod
时,这成为目标,并且调用给定方法的基类版本。这是一个非常简单的示例
class Speaker {
public void speak() {
System.out.println("Hello, world!");
}
}
class ConcurrentSpeaker extends Speaker {
public void speak() {
Thread t = new Thread(super::speak);
t.start();
}
}
当线程启动时,run
方法被调用,super::speak
被执行,调用其基类的 speak
方法。请注意,在内部类中,您可以将封闭类的 this
引用捕获为 EnclosingClass.this::method
或 EnclosingClass.super::method
。
构造函数引用
构造函数引用与方法引用相同,只是方法名称是 new
。例如,Button::new
是 Button
类的构造函数引用。将调用哪个构造函数取决于上下文。想象一下,您想将字符串列表转换为按钮数组。在这种情况下,您应该对每个字符串调用构造函数。它可能类似于这样
List<String> strs = ...;
Stream<Button> stream = strs.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());
有关 stream
、map
和 collect
方法的更多信息,您可以查看文档。在这种情况下,最重要的是 map
方法为每个列表元素调用 Button(String)
构造函数。有多个 Button
构造函数,但编译器选择带有 String
参数的构造函数,因为从上下文可以清楚地看出应该调用带有字符串的构造函数。
还可以为数组类型创建构造函数引用。例如,int
数组的构造函数引用是 int[]::new
。它接受一个参数:数组的长度。它等价于 lambda 表达式 x -> new int[x]
。
数组的构造函数引用对于超越 Java 限制很有用。无法创建泛型类型 T
的数组。表达式 new T[n]
是不正确的,因为它将被替换为 new Object[n]
。这对库作者来说是一个问题。想象一下,我们想要一个按钮数组。Stream
类中有一个 toArray
方法,它返回一个 Object
数组
Object[] buttons = stream.toArray();
但那不是你想要的。用户不想要 Objects
,只想要按钮。库使用构造函数引用解决了这个问题。您应该将 Button[]::new
传递给 toArray
方法
Button[] buttons = stream.toArray(Button[]::new);
toArray
方法调用此构造函数以获取所需类型的数组,然后在填充后返回此数组。
变量作用域
通常,您希望在 lambda 表达式中访问封闭作用域中的变量。考虑这段代码
public static void repeatText(String text, int count) {
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
考虑一个调用
repeatText("Hi!", 2000); // Prints Hi 2000 times in a separate thread
请注意,变量 count
和 text
未在 lambda 表达式中定义;它们是封闭方法的参数。
如果你仔细看这段代码,你会发现幕后有一些“魔术”。repeatText
方法可能在 lambda 表达式的代码运行之前返回,届时参数变量已经消失,但它们仍然可供 lambda 使用。秘密是什么?
为了弄清楚发生了什么,我们需要更好地理解 lambda 表达式。lambda 表达式由三个组件组成
- 一个代码块
- 参数
- 未作为参数且未在 lambda 内部定义的自由变量
在我们的例子中,有两个自由变量,text
和 count
。代表 lambda 的数据结构必须存储它们的值,即“Hi!”和 2000。它们被称为被 lambda 表达式捕获。(如何实现取决于具体实现。例如,实现可以将 lambda 表达式转换为只有一个方法的对象,并将自由变量的值复制到该对象的实例变量中。)
有一个特殊的术语“闭包”;它是一个代码块以及自由变量的值。Java 中的 lambda 提供了方便的语法来表示闭包。顺便说一句,内部类一直都是闭包。
因此,lambda 表达式可以捕获封闭作用域中变量的值,但对此有一些限制。您不能更改捕获变量的值。以下代码不正确
public static void repeatText(String text, int count) {
Runnable r = () -> {
while (count > 0) {
count--; // Error: Can't modify captured variable
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}
此限制是合理的,因为在 lambda 表达式中修改变量不是线程安全的。想象一下我们有一系列并发任务,每个任务都在更新一个共享计数器。
int matchCount = 0;
for (Path p : files)
new Thread(() -> { if (p has some property) matchCount++; }).start();
// Illegal to change matchCount
如果这段代码是正确的,那将非常非常糟糕。增量运算符 ++
不是原子操作,并且如果多个线程并发执行此代码,则无法控制结果。
内部类也可以从外部类中捕获值。在 Java 8 之前,内部类只能访问 final
局部变量。此规则已扩展以匹配 lambda 表达式的规则。现在内部类可以处理任何值不会改变的变量(实际上是 final
变量)。
不要指望编译器能捕获所有并发访问错误。您应该知道,这条禁止修改的规则仅适用于局部变量。如果我们处理外部类的实例变量或静态变量,则不会出现错误,结果是未定义的。
即使不安全,您也可以修改共享对象。例如,
List<Path> matchedObjs = new ArrayList<>();
for (Path p : files)
new Thread(() -> { if (p has some property) matchedObjs.add(p); }).start();
// Legal to change matchedObjs, but unsafe
如果您仔细观察,您会发现变量 matchedObjs
实际上是 final 的。(实际上是 final 的变量是指在初始化后从未被赋予新值的变量。)在这段代码中,matchedObjs
始终引用同一个对象。然而,变量 matchedObjs
被修改了,而且它不是线程安全的。在多线程环境中运行这段代码的结果是不可预测的。
对于此类多线程任务,存在安全的机制。例如,您可以使用线程安全的计数器和集合,以及流来收集值。
在内部类的情况下,有一个变通方法允许 lambda 表达式更新封闭局部作用域中的计数器。想象一下,您使用一个长度为 1 的数组,像这样
int[] counts = new int[1];
button.setOnAction(event -> counts[0]++);
很明显,这段代码不是线程安全的。对于按钮回调,这并不重要,但一般来说,在使用这个技巧之前,您应该三思。
lambda 的主体与嵌套块具有相同的范围。名称冲突和遮蔽的规则在这里是相同的。您不能在 lambda 中声明与封闭范围中的变量同名的参数或局部变量。
Path first = Paths.get("/usr/local");
Comparator<String> comp =
(first, second) -> Integer.compare(first.length(), second.length());
// Error: Variable first already defined
您不能在一个方法内部拥有两个同名的局部变量。因此,您也不能在 lambda 表达式中引入这样的变量。相同的规则也适用于 lambda。当您在 lambda 内部使用 this
关键字时,您指的是创建 lambda 的方法的 this
。让我们考虑这段代码
public class Application() {
public void doSomething() {
Runnable r = () -> { ...; System.out.println(this.toString()); ... };
...
}
}
在此示例中,this.toString()
调用的是 Application
对象的 toString
方法,而不是 Runnable
实例的。在 lambda 表达式中使用 this
并没有什么特别之处。lambda 表达式的作用域嵌套在 doSomething
方法内部,并且 this
在该方法中的任何地方都具有相同的含义。
默认方法
现在,最后,让我们谈谈一个与 lambda 没有直接关系,但也非常有趣的新特性——默认方法。
在许多编程语言中,函数表达式与它们的集合库集成在一起。这通常导致代码比使用循环的等效代码更短且更容易理解。让我们考虑一下代码
for (int i = 0; i < strList.size(); i++)
System.out.println(strList.get(i));
我们可以改进这段代码。库可以提供一个方便的 forEach
方法,该方法将一个函数应用于每个元素。因此您可以简化代码
strList.forEach(System.out::println);
如果库是从头开始设计的,一切都很好,但如果它是在很久以前创建的,就像 Java 中一样呢?如果 Collection
接口添加了一个新方法,例如 forEach
,那么每个定义自己实现此接口的应用程序都将无法工作,除非它也实现了该方法。这在 Java 中是不好的。
Java 中的这个问题通过允许接口方法具有具体实现(称为默认方法)来解决。您可以安全地将此类方法添加到现有接口中。现在我们将更仔细地研究默认方法。在 Java 8 中,forEach
方法已使用您将在下面看到的技巧添加到 Iterable
(Collection
的基本接口)中。
考虑这个接口
interface Person {
long getId();
default String getFirstName() { return "Jack"; }
}
接口中有两个方法:getId
是一个抽象方法,而 getFirstName
是默认方法。实现此接口的具体类当然必须提供 getId
的实现,但它可以选择使用 getFirstName
的默认实现或重写它。
这种技术停止使用经典的模式,即提供一个接口和一个抽象类,该抽象类实现其大部分或所有方法,例如 Collection/AbstractCollection
或 WindowListener/WindowAdapter
。现在,您可以在接口中实现所需的方法。
如果一个方法具有相同的签名,在一个接口中被定义为默认方法,然后在基类或另一个接口中再次被定义,会发生什么?其他语言(如 C++)有复杂的规则来解决这种歧义。幸运的是,Java 中的规则要简单得多。让我们来看看它们
- 基类获胜。如果基类包含一个具体方法,则具有相同签名的默认方法将被忽略。
- 接口冲突。如果一个基接口有一个默认方法,而另一个接口包含一个具有相同签名的方法(无论是默认方法还是非默认方法),那么您必须通过覆盖该方法手动解决冲突。
让我们仔细看看第二条规则。想象另一个带有 getFirstName
方法的接口
interface Naming {
default String getFirstName() { return getClass().getName() + "_" + hashCode(); }
}
如果您尝试创建一个同时实现这两个接口的类会发生什么?
class Student implements Person, Naming {
...
}
这个类继承了由接口 Person
和 Naming
提供的两个不同的 getFirstName
方法。Java 编译器不是选择其中一个,而是报告一个错误,并让程序员自行解决歧义。只需在 Student
类中实现一个 getFirstName
方法。然后在这个方法中,您可以选择两个冲突方法中的一个,如下所示
class Student implements Person, Naming {
public String getFirstName() { returnPerson.super.getFirstName (); }
...
}
现在让我们考虑 Naming
接口不提供 getFirstName
的默认实现的情况
interface Naming {
String getFirstName();
}
Student
类会继承 Person
接口的默认方法吗?Java 设计师选择了统一性,尽管这种方式可能合理。无论接口如何冲突。如果至少一个接口包含实现,编译器会报告冲突,程序员必须手动解决问题。
如果没有任何接口提供该方法的默认实现,那么就没有问题。一个最终类可以实现该方法,也可以不实现。在第二种情况下,类本身仍然是抽象的。
我刚刚描述了两个接口之间的名称冲突。现在让我们考虑一个类,它继承了另一个类并实现了一个接口,从两者都继承了相同的方法。例如,假设 Person
是一个类,Naming
是一个接口,并且类 Student
定义如下
class Student extends Person implements Naming { ... }
在这种情况下,接口中的任何默认方法都会被简单地忽略。在我们的示例中,Student
从 Person
继承了 getFirstName
方法,而 Naming
接口是否为 getFirstName
提供默认实现并不重要。这就是“类获胜”规则的作用。这条规则确保了与 Java 7 的兼容性。如果您向接口添加默认方法,它对旧代码没有影响。但请记住,您永远不能创建覆盖 Object
类中任何方法的默认方法。例如,您不能为 toString
或 equals
重新定义默认方法,尽管这对于 List
等接口可能有用。由于该规则,这样的方法永远无法胜过 Object.toString
或 Object.equals
。
默认方法允许您向现有接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性。特别是,使用默认方法,您可以向现有接口添加接受 lambda 表达式作为参数的方法。
结论
我将 lambda 表达式描述为编程模型有史以来最大的升级——甚至可能比泛型还要大。为什么不呢?它让 Java 程序员获得了与其他函数式编程语言相比所缺乏的优势。除了默认方法等其他特性外,lambda 还可以用来编写一些非常好的代码。
我希望这篇文章能让大家一睹 Java 8 能为我们带来什么。