Java 8 Lambda限制:闭包

摘要: 主要讨论一下java8 lambda的一些限制和闭包的概念,以及简单比较一下java和javascripe实现闭包的区别。java

假设咱们想建立一个简单的线程,只在控制台上打印一些东西:编程

int answer = 42;
Thread t = new Thread(
    () -> System.out.println("The answer is: " + answer)
);

若是咱们想在线程里面修改answer的值怎么办?后端

在本文中,我想回答这个问题,讨论Java lambda表达式的限制和沿途的后果。缓存

简单的答案是Java实现闭包,可是当咱们将它们与其余语言进行比较时会有限制。另外一方面,这些限制能够被认为是可忽略的。服务器

为了支持这种说法,我将展现闭包在JavaScript这一著名语言中起着相当重要的做用。微信

Java 8 Lambda表达式从哪里来?

在过去,实现上述示例的紧凑方法是建立一个新的Runnable匿名类的实例,以下所示:闭包

int answer = 42;
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("The answer is: " + answer);
    }
});

从Java 8开始,上一个例子可使用lambda表达式编写。函数

如今,咱们都知道Java 8 lambda表达式不只仅是为了下降代码的冗长性,他们还有不少其余的新功能。此外,在匿名类和lambda表达式的实现之间存在差别。this

可是,主要的一点我想在此强调的是,考虑到他们在封闭范围如何交互,咱们能够认为它们只是一种建立匿名类接口的紧凑方式,好比 RunnableCallableFunctionPredicate ,等。实际上,lambda表达式和它的封闭范围之间的相互做用保持彻底相同(即this 关键字语义上的差别 )。spa

Java 8 Lambda限制

Java中的lambda表达式(以及匿名类)只能访问封闭范围的最终(或实际上最终)变量。

例如,考虑如下示例:

void fn() {
    int myVar = 42;
    Supplier<Integer> lambdaFun = () -> myVar; // error
    myVar++;
    System.out.println(lambdaFun.get());
}

这不会编译,由于增量myVar阻止它是实际上最终变量。

JavaScript及其功能

JavaScript中的函数和lambda表达式使用闭包的概念:

“闭包是一种特殊类型的对象,它结合了两个东西:一个函数,以及建立该函数的环境。环境包括在建立闭包时在范围内的任何局部变量” - MDN

事实上,前面的例子在JavaScript中工做得很好。

function fn() { // the enclosing scope
    var myVar = 42;
    var lambdaFun = () => myVar;
    myVar++;
    console.log(lambdaFun()); // it prints 43
}

此示例中的lambda函数使用的已更改值的myVar

实际上,在JavaScript中,一个新函数维护一个指向它所定义的封闭范围的指针。这个基本机制容许建立闭包,这保存了自由变量的存储位置 - 这些能够由函数自己以及其余函数修改。

Java建立闭包?

Java只保存自由变量的值,让它们在lambda表达式中使用。即便有一个增量myVar,lambda函数仍然会返回42.编译器避免了那些不相干的状况的建立,限制能够在lambda表达式(和匿名类)内部使用的变量的类型只有最终的和实际上最终的。

尽管有这个限制,咱们可使用Java 8实现闭包。事实上,闭包更多的是理论上的理解,只捕获自由变量的价值。在纯函数语言中,这应该是惟一容许的,保持引用透明度属性。

后来,一些功能语言以及诸如Javascript之类的语言引入了捕获自由变量的存储位置的可能性。这容许引入反作用的可能性。

因此,咱们能够说,使用JavaScript的闭包,咱们能够作更多。可是,这些反作用如何真正帮助JavaScript?他们真的很重要吗?

反作用和JavaScript

为了更好地理解闭包的概念,如今考虑下面的JavaScript代码(forgive在JavaScript中,这能够以很是紧凑的方式完成,但我想它看起来像Java并进行比较):

function createCounter(initValue) { // the enclosing scope
    var count = initValue;
    var map = new Map();
    map.set('val', () => count);
    map.set('inc', () => count++);
    return map;
}
v = createCounter(42);
v.get('val')(); // returns 42
v.get('inc')(); // returns 42
v.get('val')(); // returns 43

每次 createCounter 调用时,它都会建立一个具备两个新lambda函数的映射,它们分别返回和递增在封闭范围中定义的变量值。

换句话说,第一个函数具备改变另外一个函数的结果的反作用。

这里要注意的一个重要事实是,它createCounter的做用域在它的终止以后仍然存在,而且同时被两个lambda函数使用。

反作用和Java

如今让咱们尝试在Java中作一样的事情:

public static Map<String, Supplier> createCounter(int initValue) { // the enclosing scope
    int count = initValue;
    Map<String, Supplier> map = new HashMap<>();
    map.put("val", () -> count);
    map.put("inc", () -> count++);
    return map;
}

此代码不会编译,由于第二个lambda函数试图更改变量count

Java将函数变量(例如count)存储在堆栈中; 那些被删除与终止createCounter。建立的lambdas使用的复制版本count。若是编译器容许第二个lambda改变它的复制版本count ,那将是很混乱。

Java闭包使用可变对象

正如咱们所看到的,使用的变量的值被复制到lambda表达式(或匿名类)。可是,若是咱们使用对象呢?在这种状况下,只有引用将被复制,咱们能够看看有点不一样的东西。

咱们几乎能够用如下方式模拟JavaScript的闭包的行为:

private static class MyClosure {
    public int value;
    public MyClosure(int initValue) { this.value = initValue; }
}
public static Map<String, Supplier> createCounter(int initValue) {
    MyClosure closure = new MyClosure(initValue);
    Map<String, Supplier> counter = new HashMap<>();
    counter.put("val", () -> closure.value);
    counter.put("inc", () -> closure.value++);
    return counter;
}
Supplier[] v = createCounter(42);
v.get("val").get(); // returns 42
v.get("inc").get(); // returns 42
v.get("val").get(); // returns 43

事实上,这不是真正有用的东西,它真的是不太优雅

闭包做为建立对象的机制

JavaScript使用闭包做为建立“类”实例:对象的基本机制。这就是为何在JavaScript中,相似的函数MyCounter称为“构造函数”。

相反,Java已经有类,咱们能够以更优雅的方式建立对象。

在前面的例子中,咱们不须要一个闭包。“工厂函数”本质上是一个类定义的奇怪的例子。在Java中,咱们能够简单地定义一个类,以下所示:

class MyJavaCounter {
    private int value;
    public MyJavaCounter(int initValue) { this.value = initValue; }
    public int increment() { return value++; }
    public int get() { return value; }
}
MyJavaCounter v = new MyJavaCounter(42);
System.out.println(v.get());       // returns 42
System.out.println(v.increment()); // returns 42
System.out.println(v.get());       // returns 43

修改自由变量是一个坏习惯

修改自由变量(即在lambda函数以外定义的任何对象)的Lambda函数可能会产生混淆。其余功能的反作用可能会致使没必要要的错误。

这是典型的老年语言的开发人员不明白为何JavaScript产生随机的莫名其妙的行为。 在功能语言中,它一般是有限的,而当它不是,则不鼓励。

考虑你正在使用并行范例,例如在Spark中:

int counter = 0;
JavaRDD rdd = sc.parallelize(data);
rdd.foreach(x -> counter += x); // Don't do this!!

结论

咱们已经看到了一个很是简要的Java 8 lambda表达式。咱们专一于匿名类和lambda表达式之间的区别。以后,咱们更好地看到了闭包的概念,看看它们是如何在JavaScript中实现的。此外,咱们看到JavaScript的闭包不能直接在Java 8中使用,以及如何经过对象引用来模拟它。

咱们还发现,当咱们将它们与JavaScript之类的语言进行比较时,Java对闭包的支持有限。

然而,咱们看到这些限制并不真正重要。事实上,闭包在JavaScript中被用做定义类和建立对象的基本机制,咱们都知道它不是Java问题。


做者信息
本文系力谱宿云LeapCloud旗下MaxLeap团队_Service&Infra成员:贾威威 【翻译】
从过后端开发已有多年,目前主要负责MaxWon服务端部分功能的开发与设计。
原文连接:https://dzone.com/articles/ja...

相关文章
次时代Java编程(一):Java里的协程

做者往期佳做
使用Vert.x构建Web服务器和消息系统
集中式内存缓存Guava Cache

欢迎关注微信公众号
这里写图片描述

相关文章
相关标签/搜索