在 Java 编程中,咱们以不严格的术语 lambda 表达式来表示 lambda 表达式和闭包。可是在某些状况下,理解它们的区别很重要。lambda 表达式是无状态的,而闭包是带有状态的。将 lambda 表达式替换为闭包,是一种管理函数式程序中的状态的好方法。java
咱们在这个系列中介绍了 lambda 表达式,您应该已经对他们很是的了解了。它们是小巧的匿名函数,接受可选的参数,执行某种计算或操做,并且可能返回一个结果。lambda 表达式也是无状态的,这可能会在您的代码中产生重大影响。编程
咱们首先来看一个使用 lambda 表达式的简单示例。假设咱们想将一个数字集合中的偶数乘以二。一种是使用 Stream 和 lambda 表达式建立一个函数管道,入下所示:bash
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * 2)
.collect(toList());
复制代码
咱们传入 filter 中的 lambda 表达式取代了 Predicate 函数接口。它接收一个数字,若是该数字是偶数,则返回 true,不然返回 false。另外一方面,咱们传递给 map 的 lambda 表达式取代了 Function 函数接口:它接受任何数字并返回该值的两倍。这个lambda 表达式都依赖传入的参数和字面常量。两者都是独立的,这意味着他们没有任何外部依赖项。由于它们依赖于传入的参数,并且可能还依赖于一些常量,因此 lambda 表达式是无状态的。闭包
如今让咱们更仔细地看看传递给 map 方法的 lambda 表达式。若是咱们但愿计算给定值的三倍或四倍,该怎么办?咱们能够将常量 2 转换为一个变量(好比 factor),但 lambda 表达式仍须要一种方式来获取该变量。函数
咱们能够推断,lambda 表达式能够采用与接收参数 e 的相同方式来接收 factor,以下所示:测试
.map((e, factor) -> e * factor)
复制代码
还不错,但不幸的是它不起做用。方法 map 要求接受函数接口 Function<T, R>
的一个实现做为参数。若是咱们传入该接口外的任何内容(好比一个 BiFunction<T, U, R>
),map 不会接受。须要采用另外一种方式将 factor 提供给咱们的 lambda 表达式。spa
函数要求变量在限定范围内。由于它们其实是匿名函数,因此 lambda 表达式也要求引用的变量在限定范围内。一些变量以参数形式被函数或 lambda 表达式接收。一些变量是局部定义的。一些变量来自函数外部,位于所谓的词法范围中。code
词法范围示例:cdn
public static void print() {
String location = "World";
Runnable runnable = new Runnable() {
public void run() {
System.out.println("Hello " + location);
}
};
runnable.run();
}
复制代码
在 print 方法中,location 是一个局部变量。可是,Runnable 的 run 方法还引用了一个不是 run 方法的局部变量或参数的 location。对 Hello 旁边的 location 的引用被绑定到 print 方法的 location 变量。对象
词法范围是函数的定义范围。反过来,它也多是该定义范围的定义范围,等等。
在前面的代码中,方法 run 没有定义 location 或接收它做为参数。run 的定义范围是 Runnable 的匿名内部对象。由于没有将 location 定义为该实例中的字段,因此会继续搜索匿名内部对象的定义范围 — 在本例中为方法 print 的局部范围。
若是 location 不在该范围中,编译器会继续在 print 的定义范围内搜索,直到找到该变量或搜索失败。
lambda表达式中的词法范围
咱们使用 lambda 表达式重写前面的代码:
public static void print() {
String location = "World";
Runnable runnable = () -> System.out.println("Hello " + location);
runnable.run();
}
复制代码
得益于 lambda 表达式,代码变得更简洁,但 location 的范围和绑定没有更改。lambda 表达式中的变量 location 被绑定到 lambda 表达式的词法范围中的变量 location。严格来说,此代码中的 lambda 表达式是一个闭包。
Lambda 表达式不依赖于任何外部实体;它们是依赖于自身参数和常量的内容。另外一方面,闭包既依赖于参数和常量,也依赖于它们的词法范围中的变量。从逻辑上讲,闭包被绑定到它们的词法范围中的变量。可是,尽管逻辑上讲是这样,但实际上并不老是这么作。有时甚至没法执行这样的绑定。两个场景能够证实这一点。
下面这段代码将一个 lambda 表达式或闭包传递给一个 call 方法:
class Sample {
public static void call(Runnable runnable) {
System.out.println("calling runnable");
//level 2 of stack
runnable.run();
}
public static void main(String[] args) {
int value = 4; //level 1 of stack
call(
() -> System.out.println(value) //level 3 of stack
);
}
}
复制代码
此代码中的闭包使用了来自它的词法范围的变量 value。若是 main 是在堆栈级别 1 上执行的,那么 call 方法的主体会在堆栈级别 2 上执行。由于 Runnable 的 run 方法是从 call 内调用的,因此该闭包的主体会在级别 3 上运行。若是 call 方法要将该闭包传递给另外一个方法(进而推迟调用的位置),则执行的堆栈级别可能高于 3。
您如今可能想知道在一个堆栈级别中的执行究竟如何能获取以前的另外一个堆栈级别中的变量 — 尤为是未在调用中传递上下文时。简单来说就是没法获取该变量。
看另一个示例:
class Sample {
public static Runnable create() {
int value = 4;
Runnable runnable = () -> System.out.println(value);
System.out.println("exiting create");
return runnable;
}
public static void main(String[] args) {
Runnable runnable = create();
System.out.println("In main");
runnable.run();
}
}
复制代码
测试结果:
exiting create
In main
4
复制代码
在这个示例中,create 方法有一个局部变量 value,该变量的寿命很短:只要咱们退出 create,它就会消失。create 内建立的闭包在其词法范围中引用了这个变量。在完成 create 方法后,该方法将闭包返回给 main 中的调用方。在此过程当中,它从本身的堆栈中删除变量 value,并且 lambda 表达式将会执行。
咱们知道,在 main 中调用 run 时,create 中的 value 就会终止。尽管咱们能够假设 lambda 表达式中的 value 直接被绑定到它的词法范围中的变量,但该假设并不成立。
闭包午休时间
假设个人办公室离家约 16 公,并且我早上 8 点出门上班。中午,我有短暂的时间用午饭,但出于健康考虑,我喜欢吃家里烹饪的饭菜。因为休息时间很短,只有在离家时带上午饭,我才能吃上家里的饭菜。
这就是闭包要完成的任务:它们携带本身的午饭(或状态)。
让咱们再看看 create 中的 lambda 表达式:
Runnable runnable = () -> System.out.println(value);
复制代码
咱们编写的 lambda 表达式没有接受任何参数,但须要它的 value。编译类 Sample 并运行 javap -c -p Sample.class 来检查字节码。您会注意到,编译器为该闭包建立了一个方法,该方法接受一个 int 参数:
private static void lambda$create$0(int);
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iload_0
4: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
7: return
}
复制代码
如今看看为 create 方法生成的字节码:
0: iconst_4
1: istore_0
2: iload_0
3: invokedynamic #2, 0 // InvokeDynamic #0:run:(I)Ljava/lang/Runnable;
复制代码
值 4 存储在一个变量中,而后,该变量被加载并传递到为闭包建立的函数。在本例中,闭包保留着 value 的一个副本。这就是闭包携带状态的方式。
如今,咱们回头看看本文开头的示例。除了计算集合中的偶数值的两倍,若是咱们想要计算它们的三倍或四倍,该怎么办?为此,咱们能够将原始 lambda 表达式转换为一个闭包。
这是咱们以前看到的无状态代码:
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * 2)
.collect(toList());
复制代码
使用闭包而不是 lambda 表达式,代码就会变成:
int factor = 3;
numbers.stream()
.filter(e -> e % 2 == 0)
.map(e -> e * factor)
.collect(toList());
复制代码
map 方法如今接受一个闭包,而不是一个 lambda 表达式。咱们知道,这个闭包接受一个参数 e,但它也捕获并携带 factor 变量的状态。
此变量位于该闭包的词法范围中。它能够是定义 lambda 表达式的函数中的局部变量;能够做为该外部函数的一个参数传入;能够位于闭包的定义范围(或该定义范围的定义范围等)中的任何地方。不管如何,该闭包将状态从定义该闭包的代码传递到了须要该变量的执行点。
闭包不一样于 lambda 表达式,由于它们依赖于本身的词法范围来获取一些变量。所以,闭包能够捕获并携带状态。lambda 表达式是无状态的,闭包是有状态的。能够在您的程序中使用闭包,将状态从定义上下文携带到执行点。
感谢 Venkat Subramaniam 博士
Venkat Subramaniam 博士站点:http://agiledeveloper.com/
知识改变命运,努力改变生活