在本章中,我们将介绍 lambdas 的理念,我们将:
- 一般讨论 lambdas 和函数式编程的一些背景知识
- 谈 Java 中的函数与类
- 查看 Java 中 lambdas 的基本语法
在我们深入了解之前,让我们先了解一下 lambdas 的一些一般背景。
如果您以前没有见过,那么在谈论 lambda 时,希腊字母λ(lambda经常被用作速记。
在计算机科学中,lambdas 回到 lambda 微积分。20 世纪 30 年代阿隆佐教堂引入的函数数学符号。这是一种利用函数探索数学的方法,后来作为计算机科学的有用工具被重新发现。
它形式化了lambda 术语的概念以及转换这些术语的规则。这些规则或函数直接映射到现代计算机科学思想中。lambda 演算中的所有函数都是匿名的,这一点在计算机科学中也得到了严格的理解。
以下是 lambda 演算表达式的示例:
一个 lambda 演算表达式
λx.x+1
This defines an anonymous function or lambda with a single argument x
. The body follows the dot and adds one to that argument.
20 世纪 50 年代,约翰·麦卡锡在麻省理工学院时发明了口齿不清。这是一种为数学问题建模而设计的编程语言,深受 lambda 演算的影响。
它使用 lambda 这个词作为操作符来定义匿名函数。
下面是一个例子:
一个 LISP 表达式
(lambda (arg) (+ arg 1))
这个 LISP 表达式的计算结果是一个函数,当应用该函数时,它将接受一个参数,将其绑定到arg
,然后向其添加1
。
这两个表达式产生相同的结果,一个递增数字的函数。你可以看到这两者非常相似。
lambda 演算和 LISP 对函数式编程产生了巨大的影响。应用函数和使用函数推理问题的思想已经直接进入编程语言。因此在我们的领域中使用了这个术语。微积分中的 lambda 与现代编程语言中的 lambda 是一样的,使用方式也是一样的。
简单来说,lambda 只是一个匿名函数。就这样。没什么特别的。这只是定义函数的一种简洁方法。当您想要传递可重用功能的片段时,匿名函数非常有用。例如,将函数传递给其他函数。
许多主流语言已经支持 lambda,包括 Scala、C#、Objective-C、Ruby、C++(11)、Python 和许多其他语言。
请记住,匿名函数与 Java 中的匿名类不同。Java 中的匿名类仍然需要实例化为对象。它可能没有合适的名称,但只有当它是对象时才有用。
另一方面,函数没有与其关联的实例。函数和它们作用的数据不相关,而对象和它作用的数据密切相关。
在现代 Java 中,您可以在任何以前使用单一方法接口的地方使用 lambdas,因此它可能看起来像语法糖,但事实并非如此。让我们看看它们之间的区别,并将匿名类与 lambdas 进行比较;类与函数。
Java pre-8 中匿名类(单个方法接口)的典型实现可能类似于此。anonymousClass
方法调用waitFor
方法,传入Condition
的一些实现;在这种情况下,它的意思是,等待某些服务器关闭:
匿名类的典型用法
void anonymousClass() {
final Server server = new HttpServer();
waitFor(new Condition() {
@Override
public Boolean isSatisfied() {
return !server.isRunning();
}
});
}
功能等效的 lambda 如下所示:
与 lambda等效的功能
void closure() {
Server server = new HttpServer();
waitFor(() -> !server.isRunning());
}
出于完整性考虑,天真的轮询waitFor
方法可能如下所示:
class WaitFor {
static void waitFor(Condition condition) throws
InterruptedException {
while (!condition.isSatisfied())
Thread.sleep(250);
}
}
首先,两种实现实际上都是闭包,后者也是 lambda。我们将在后面的Lambdas vs closures一节中更详细地介绍这一区别。这意味着两者都必须在运行时捕获其“环境”。在 JavaPre-8 中,这意味着将闭包需要的东西复制到类的实例中(条件的匿名实例)。在我们的示例中,需要将服务器变量复制到实例中。
因为它是一个副本,所以必须声明它为最终版本,以确保它在捕获和使用之间不能更改。这两个时间点可能非常不同,因为闭包通常用于将执行延迟到稍后的某个时间点(例如,请参见惰性评估。现代 Java 使用了一个巧妙的技巧,如果它能够推断变量从未更新过,那么它也可以是 final,因此它将其视为实际上是 final,而您不需要显式地将其声明为 final。
另一方面,lambda 不需要复制其环境或捕获任何术语。这意味着它可以被视为真正的函数,而不是类的实例。有什么区别?大量
首先,功能;正版函数,不需要多次实例化。我不确定在谈到分配内存和将一段机器代码作为函数加载时,实例化是否是正确的词。关键是,一旦它可用,它就可以被重用,它本质上是幂等的,因为它不保留任何状态。静态类方法是 Java 最接近函数的东西。
对于 Java 来说,这意味着不需要每次计算 lambda 时都实例化它,这是一件大事。与实例化匿名类不同,内存影响应该最小。
就一些概念上的差异而言:
- 类必须实例化,而函数不能实例化。
- 当类被更新时,内存被分配给对象。
- 内存只需为函数分配一次。它们存储在堆的永久区域。
- 对象作用于它们自己的数据,函数作用于不相关的数据。
- Java 中的静态类方法大致相当于函数。
函数和类之间的一些具体区别包括它们的捕获语义以及它们如何隐藏变量。
另一个不同点是关于捕获语义。在匿名类中,这是指匿名类的实例。例如,Foo$InnerClass
而不是Foo
。这就是为什么当您从匿名类中引用封闭范围时,会出现类似于Foo.this.x
的稍微奇怪的语法。
另一方面,在 lambdas 中,这指的是封闭范围(在我们的示例中是 Foo)。事实上,lambda 完全是词汇范围的,这意味着它们不会从超类型继承任何名称,也不会引入新的范围;您可以直接从封闭范围访问字段、方法和局部变量。
例如,此类显示 lambda 可以直接引用firstName
变量。
public class Example {
private String firstName = "Jack";
public void example() {
Function<String, String> addSurname = surname -> {
// equivalent to this.firstName
return firstName + " " + surname; // or even,
this.firstName
};
}
}
这里,firstName
是this.firstName
的简写,因为它指的是封闭范围(类Example
,所以它的值将是“Jack”。
匿名类等价物需要从封闭范围显式引用firstName
。您不能在此上下文中使用它,这意味着匿名实例不存在firstName
。因此,将编译以下内容:
public class Example {
private String firstName = "Charlie";
public void anotherExample() {
Function<String, String> addSurname = new Function<String,
String>() {
@Override
public String apply(String surname) {
return Example.this.firstName + " " + surname;
// OK
}
};
}
}
但这不会。
public class Example {
private String firstName = "Charlie";
public void anotherExample() {
Function<String, String> addSurname = new Function<String,
String>() {
@Override
public String apply(String surname) {
return this.firstName + " " + surname; // compiler error
}
};
}
}
您仍然可以直接访问该字段(也就是说,只需调用 returnfirstName + " " + surname
,但您不能使用它。这里的要点是演示在 lambdas 和匿名实例中使用时,捕获示意图的区别。
通过简化的this
语义,引用隐藏变量变得更加直截了当。例如
public class ShadowingExample {
private String firstName = "Charlie";
public void shadowingExample(String firstName) {
Function<String, String> addSurname = surname -> {
return this.firstName + " " + surname;
};
}
}
这里,因为this
在 lambda 内部,所以它指的是封闭范围。因此this.firstName
将具有值"Charlie"
,而不是同名的方法参数。捕获语义使它更清晰。如果您使用firstName
(并删除this
,它将引用该参数。
在下一个示例中,使用匿名实例,firstName
只是引用参数。如果您想参考随附的版本,您可以使用Example.this.firstName
:
public class ShadowingExample {
private String firstName = "Charlie";
public void anotherShadowingExample(String firstName) {
Function<String, String> addSurname = new Function<String,
String>() {
@Override
public String apply(String surname) {
return firstName + " " + surname;
}
};
}
}
从学术意义上讲,函数与匿名类(我们通常将匿名类视为 JavaPre-8 中的函数)截然不同。理解这些区别是很有用的,这样可以证明 lambdas 的使用不仅仅是为了简洁的语法。当然,使用 lambdas 还有很多额外的优势(尤其是对 JDK 进行了改造以大量使用它们)。
当我们接下来研究新的 lambda 语法时,请记住,尽管 lambda 的使用方式与 Java 中的匿名类非常相似,但它们在技术上是不同的。与匿名类的实例不同,Java 中的 lambda 不需要每次求值时都实例化。
这应该提醒您,Java 中的 lambda 不仅仅是语法糖。
让我们来看看基本的 lambda 语法。
lambda 基本上是一个匿名的功能块。这很像使用匿名类实例。例如,如果我们想在 Java 中对数组进行排序,我们可以使用Arrays.sort
方法,该方法以Comparator
接口为例。
它看起来像这样:
Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});
这里的Comparator
实例是功能的抽象部分;它本身毫无意义;只有当sort
方法使用它时,它才有目的。
使用 Java 的新语法,可以将其替换为 lambda,如下所示:
Arrays.sort(numbers, (first, second) -> first.compareTo(second));
这是实现同样目标的更简洁的方法。事实上,Java 将其视为Comparator
类的实例。如果我们要为 lambda(第二个参数)提取一个变量,它的类型将是Comparator<Integer>
,就像上面的匿名实例一样。
Comparator<Integer> ascending = (first, second) -> first.compareTo(second);
Arrays.sort(numbers, ascending);
因为Comparator
只有一个单一的抽象方法;compareTo
,编译器可以拼凑起来,当我们有这样一个匿名块时,我们实际上是指Comparator
的一个实例。它可以做到这一点,这要归功于我们稍后将讨论的其他几个新特性;功能接口和类型推断的改进。
您始终可以从使用单个抽象方法转换为使用 lambda 的抽象方法。
假设我们有一个带有方法apply
的接口Example
,返回一些类型并接受一些参数:
interface Example {
R apply(A arg);
}
我们可以用如下方式实例化一个实例:
new Example() {
@Override
public R apply(A args) {
body
}
};
要转换成 lambda,我们基本上要修剪脂肪。我们删除了实例化和注释,删除了只剩下参数列表和主体的方法细节。
(args) {
body
}
然后,我们引入新的箭头符号来表示整个对象是 lambda,下面是主体,这是我们的基本 lambda 语法:
(args) -> {
body
}
让我们通过这些步骤,以前面的排序示例为例。我们从匿名实例开始:
Arrays.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer first, Integer second) {
return first.compareTo(second);
}
});
并修剪实例化和方法签名:
Arrays.sort(numbers, (Integer first, Integer second) {
return first.compareTo(second);
});
介绍 lambda
Arrays.sort(numbers, (Integer first, Integer second) -> {
return first.compareTo(second);
});
我们完成了。我们可以做一些优化。如果编译器知道足够的信息来推断类型,则可以删除这些类型。
Arrays.sort(numbers, (first, second) -> {
return first.compareTo(second);
});
对于简单表达式,可以删除大括号以生成 lambda 表达式:
Arrays.sort(numbers, (first, second) -> first.compareTo(second));
在这种情况下,编译器可以推断出足够的信息来了解您的意思。这个语句返回一个与接口一致的值,所以它说,“不需要告诉我你要返回什么,我自己可以看到”。
对于单参数接口方法,甚至可以删除第一个括号。例如,lambda 接受一个参数x
并返回x + 1
;
(x) -> x + 1
可以不用括号写
x -> x + 1
让我们总结一下语法选项。
语法摘要:
(int x, int y) -> { return x + y; }
(x, y) -> { return x + y; }
(x, y) -> x + y; x -> x * 2
() -> System.out.println("Hey there!");
System.out::println;
第一个例子((int x, int y) -> { return x + y; })
是创建 lambda 最详细的方法。函数的参数及其类型在括号中,后跟新的箭头语法,然后是正文;要执行的代码块。
您通常可以从参数列表中删除类型,如(x, y) -> { return x + y; }
。编译器将在这里使用类型推断来尝试和猜测类型。它是基于您试图在其中使用 lambda 的上下文来实现的。
如果代码块返回某个内容或是单行表达式,则可以删除大括号和 return 语句,例如(x, y) -> x + y;
。
如果只有一个参数,可以去掉括号x -> x * 2
。
如果你没有任何理由,就需要“汉堡包”符号,
() -> System.out.println("Hey there!");
。
为了完整性,还有另一种变化;一种到 lambda 的快捷方式,称为方法引用。一个例子是类似于System.out::println;
的东西,它基本上是通向 lambda(value -> System.out.prinltn(value)
的捷径。
我们将在后面更详细地讨论方法引用,所以现在,请注意它们存在并且可以在任何可以使用 lambda 的地方使用。