class TryUsingAnonymousClass$1
implements MyInterface {
private final TryUsingAnonymousClass this$0;
private final Integer paramInteger;
TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer paramInteger) {
this.this$0 = this$0;
this.paramInteger = paramInteger;
public void doSomething() {
System.out.println(this.paramInteger);
可以看到名为number的局部变量是作为构造方法的参数传入匿名内部类的(以上代码经过了手动修改,真实的反编译结果中有一些不可读的命名)。
如果Java允许匿名内部类访问非final的局部变量的话,那我们就可以在TryUsingAnonymousClass$1中修改paramInteger,但是这不会对number的值有影响,因为它们是不同的reference。
这就会造成数据不同步的问题。
作者:RednaxelaFX
链接:https://www.zhihu.com/question/27416568/answer/36565794
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
闭包与对象是从两个完全不同的角度描述了一件事情:一段代码与其环境的关系。
所以可以用对象来模拟(实现)闭包,也可以用闭包来模拟(实现)对象。
用既有闭包又有对象的JavaScript来举例,
用闭包模拟对象的例子:
function makeCounter() {
var count = 0;
var incr = function () { count++; }
var decr = function () { count--; }
var value = function () { return count; }
var dispatch = function (name) {
switch (name) {
case "incr": return incr;
case "decr": return decr;
case "value": return value;
default: return null;
return dispatch;
var counter = makeCounter();
var v = counter("value")(); // 0
console.log(v);
counter("incr")();
counter("incr")();
counter("incr")();
counter("decr")();
v = counter("value")(); // 2
console.log(v);
这段代码完全没有用JavaScript内建的对象和对象属性访问机制(这里假定不把function看作对象嗯),纯用闭包来模拟了“对象”的行为。语法上不太好看但意思应该很明确。
反过来,用对象模拟闭包的例子,有几个现成的,我就不在这里写了。一个例子:Optimizing JavaScript variable access,用Map对象来模拟JavaScript的scope(也就是闭包的环境部分的实体)。
然后请看个我之前做的分享:SDCC 2012上做的JVM分享
主要是想给题主感受一下:It can always be done。
结合上面两点,答案就很清晰了:JVM有原生的基于类的对象支持,所以在JVM上实现一种支持闭包的语言只需要让该语言的编译器生成模拟闭包的类即可。
让我们考察下面两种情况:
只有值捕获(capture-by-value):只需要在创建闭包的地方把捕获的值拷贝一份到对象里即可。Java的匿名内部类和Java 8新的lambda表达式都是这样实现的。
有引用捕获(capture-by-reference):把被捕获的局部变量“提升”(hoist)到对象里。C#的匿名函数(匿名委托/lambda表达式)就是这样实现的。参考Eric Lippert大神对“hoist”一词的讲解。不要把这个“hoist”的用法跟JavaScript里说的把局部变量提前到函数开头来声明的那种用法混为一谈。
如果变量(variable)是不可变(immutable)的,那么使用者无法感知值捕获和引用捕获的区别。
有些语言(例如C++11)允许显式指定捕获列表以及捕获方式(值捕获还是引用捕获),这样最清晰,不过写起来比较长;
有些语言(例如JavaScript)只有引用捕获,要模拟值捕获的效果需要手动新建闭包和局部变量;
有些语言(例如C#)对不可变量(const local)做值捕获,对普通局部变量做引用捕获;由于无法感知对不可变量的值捕获与引用捕获的区别,统一把这个行为描述成是引用捕获更方便一些。
有些语言(例如Java)虽然目前只实现了值捕获,但是还要留着面子不承认自己只做了值捕获,所以只允许捕获不变量(final local),或者例如Java 8允许捕获事实上不变量(effectively final local)。这样,虽然实现用的是值捕获,但效果看起来跟引用捕获一样;就算以后的Java扩展到允许通用的(对可变变量的)引用捕获,也不会跟已有的代码发生不兼容。
有些语言(例如Python)的lambda略奇葩,实现的是引用捕获,但是lambda内不能对捕获的变量赋值,只有原本定义那些变量的作用域里能对它们赋值。
回头再更新一些例子上来,讲讲具体的实现。有许多有趣的例子,例如说编译器可以区分哪些局部变量被捕获了而哪些没有;被引用捕获的局部变量既可以打包提升到同一个对象实例里(C#的做法),也可以每个变量单独提升到自己的一个对象里(Lua的upvalue做法);捕获和提升的时机也有一些选择范围。
Closures in Lua
看LuaJ实现的UpValue,非常直观的实现了Lua思路的upvalue——每个被捕获变量有自己的upvalue对象实例。
作者:RednaxelaFX
链接:https://www.zhihu.com/question/28190927/answer/39786939
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Java 8语言上的lambda表达式只实现了capture-by-value,也就是说它捕获的局部变量都会拷贝一份到lambda表达式的实体里,然后在lambda表达式里要变也只能变自己的那份拷贝而无法影响外部原本的变量;但是Java语言的设计者又要挂牌坊不明说自己是capture-by-value,为了以后语言能进一步扩展成支持capture-by-reference留下后路,所以现在干脆不允许向捕获的变量赋值,而且可以捕获的也只有“效果上不可变”(effectively final)的参数/局部变量。
关于Java闭包的讨论可以参考我之前的另一个回答:JVM的规范中允许编程语言语义中创建闭包(closure)吗? - RednaxelaFX 的回答
但是Java只是不允许改变被lambda表达式捕获的变量,并没有限制这些变量所指向的对象的状态能不能变。要从lambda表达式向外传值的常见workaround之一就是用长度为1的数组:
String[] a = new String[1];
... ( () -> a[0] = "a" );
return a[0];
JDK内部自己都有些代码这么做嗯…
这种做法可以叫做“手动boxing”。那个长度为1的数组其实就是个Box。
顺带一提,.NET标准库里也有类似这样的box,例如StrongBox(T) Class (System.Runtime.CompilerServices)。强大如C#,这种box仍然有它的用途,例如配合DLR的expression tree使用——跟C#语言倒没啥关系。
当然后面的回答提到的一个观点也值得强调一下:带副作用的lambda表达式请小心使用。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
喜欢就支持一下吧