Java 8 习惯用语(10):使用闭包捕获状态

Java 8 习惯用语(10):使用闭包捕获状态

在 Java™ 编程中,咱们以不严格地使用术语 lambda 表达式来表示 lambda 表达式和闭包。但在某些状况下,理解它们的区别很重要。lambda 表达式是无状态的,而闭包是带有状态的。将 lambda 表达式替换为闭包,是一种管理函数式程序中的状态的好方法。html

无状态的生活

咱们在本系列中大量介绍了 lambda 表达式,您应该已经对它们有很是透彻的了解。它们是小巧的匿名函数,接受可选的参数,执行某种计算或操做,并且可能返回一个结果。lambda 表达式也是无状态的,这可能会在您的代码中产生重大影响。java

咱们首先看一个使用 lambda 表达式的简单示例。假设咱们想将一个数字集合中的偶数乘以二。一种选择是使用 Stream 和 lambda 表达式建立一个函数管道,以下所示:编程

numbers.stream()
 .filter(e -> e % 2 == 0)
 .map(e -> e * 2)
 .collect(toList());

咱们传入 filter 中的 lambda 表达式取代了 Predicate 函数接口。它接受一个数字,若是该数字是偶数,则返回 true,不然返回 false。另外一方面,咱们传递给 map 的 lambda 表达式取代了 Function 函数接口:它接受任何数字并返回该值的两倍。闭包

这两个 lambda 表达式都依赖于传入的参数和字面常量。两者都是独立的,这意味着它们没有任何外部依赖项。由于它们依赖于传入的参数,并且可能还依赖于一些常量,因此 lambda 表达式是无状态的。它们很可爱,也很安静,就像熟睡的婴儿同样。ide

咱们为何须要状态

如今让咱们更仔细地看看传递给 map 方法的 lambda 表达式。若是咱们但愿计算给定值的三倍或四倍,该怎么办?咱们能够将常量 2 转换为一个变量(好比 factor),但 lambda 表达式仍须要一种方式来获取该变量。函数

咱们能够推断,lambda 表达式能够采用与接收参数 e 的相同方式来接收 factor,以下所示:code

.map((e, factor) -> e * factor)

还不错,但不幸的是它不起做用。方法 map 要求接受函数接口 Function<T, R> 的一个实现做为参数。若是咱们传入该接口外的任何内容(好比一个 BiFunction<T, U, R>),map 不会接受。须要采用另外一种方式将 factor 提供给咱们的 lambda 表达式。htm

词法范围

函数要求变量在限定范围内。由于它们其实是匿名函数,因此 lambda 表达式也要求引用的变量在限定范围内。一些变量以参数形式被函数或 lambda 表达式接收。一些变量是局部定义的。一些变量来自函数外部,位于所谓的词法范围中。对象

下面是一个词法范围示例。blog

public static void print() {
 String location = "World";
  
 Runnable runnable = new Runnable() {
 public void run() {
 System.out.println("Hello " + location);
 }
 };
  
 runnable.run();}

print 方法中,location 是一个局部变量。可是,Runnablerun 方法还引用了一个不是 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 上执行。由于 Runnablerun 方法是从 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();
 }}

在这个示例中,create 方法有一个局部变量 value,该变量的寿命很短:只要咱们退出 create,它就会消失。create 内建立的闭包在其词法范围中引用了这个变量。在完成 create 方法后,该方法将闭包返回给 main 中的调用方。在此过程当中,它从本身的堆栈中删除变量 value,并且 lambda 表达式将会执行。这是结果输出:

exiting createIn main4

咱们知道,在 main 中调用 run 时,create 中的 value 就会终止。尽管咱们能够假设 lambda 表达式中的 value 直接被绑定到它的词法范围中的变量,但该假设并不成立。

可经过一个类比来揭示其中原委。

闭包午休时间

假设个人办公室离家约 10 英里(使用改进的测量单位的话为 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_41: istore_02: iload_03: 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  
原文连接: Java 8 习惯用语
原出处: IBM Developer

e3f6982e97de58e289858a0ec142affe.jpeg

相关文章
相关标签/搜索