闭包这个概念,广泛存在于计算机(尤其是前端编程JavaScript中)和数学领域中;不可否认它是一个非常重要的概念,但是它现在似乎变成了一个千人千面的词汇,闭包似乎就是 lambda表达式,是回调,是函数式编程,是一种更高级的封装形式,是惰性计算..... 我觉得是把使用闭包这一思想可以达到的一些效果,混为一谈,导致这个简单的概念变得如此复杂;
在数学上,集合分支中有一种集合叫做闭包集合; 就是对集合中的所有元素做一些运算操作,得到的结果仍然在集合中,那么这个计算就称为闭包集合; 比如 自然数 集合对于 加法 这种操作而言就是一个闭包;
我们把上述数学概念用一种比较模糊哲学的语言描述,一个元素(可以是对象,数值,一个计算式/Function)以及它所相关的元素(即上下文)所形成的一个整体/集合 即为一个闭包(它至少可以达成某些目的); 上述是我个人对闭包这一概念的定义,我也觉得它就是闭包含义的全部;
- 在面向对象语言编程中,一个普通的对象是不是一个闭包? 绝大多数可能不是, 结合上述的定义,它并没有包含它的上下文; 它只是单一元素;
- 内部类是不是一个闭包? 是,它是一个闭包!内部类的外部类是可以访问它的外部类的;内部类对象和它相关的外部类对象,以及构造它的函数中的那些被用到的局部变量在一起构成了一个闭包;
- 静态内部类是不是一个闭包?我认为不是,静态内部类其实是与他的外部类独立存在;静态内部类也不需要引用任何外部类的状态,它自己也不保有状态;内部类也禁止在其内部创建静态静态变量,我觉得这并非是编译技术上的限制,而是如果允许创建静态内部类是对闭包语义的错误理解,静态变量不能视作当前执行的上下文(它不是动态的);
- JavaScript 返回的function 闭包有什么特殊之处?我认为没有特殊之处,它符合上面的定义,返回这个function本身 和 以及 它相关的上下文元素(在javascript中好像叫做调用链);这与java 的内部类所达成的效果在概念上是一致的,但是因为实现的手法不同,所以导致用起来会有些许差异(比如 Java的内部类所使用的外部元素必须定义/实质为final类型,这是由java实现这一内部类闭包功能时手法所收到的限制);
- 函数回调/惰性执行 与 闭包? 我觉得他们都是可以用闭包这一技术而能达成的效果(不用闭包也能达成);一般的函数/表达式,调用它时,即要执行它的功能,它要返回我结果;而一些函数,它返回我的一个元素(是函数,是对象(内部类)并不重要)以及执行它所需要的上下文,交给调用者恰当的时机调用它即可;最典型的java Builder模式构建对象,可以将它理解为惰性构建,但是Builder对象不是闭包;
- 函数式编程/lambda表达式: 我认为没关系,它们只是在编程写法上的一次改进,简化了写法,也便于阅读者理解;一般我们写闭包都是一些简单的执行内容,所以天然适合使用lamdba表达式;
- Java的假闭包: 这是因为Java 内部类实现闭包的手法导致它的闭包是受限的(最典型就是必须内部类只能使用申明为final 类型成员变量)。虽然实现上并不是那么闭包(上下文是隐含的传入构造体,构造的还是普通对象),但是在我看来,它还是闭包,它满足了保存当前元素,以及它上下文元素的闭包含义;
- 闭包导致内存泄漏:内存泄漏 和 (局部)变量升格(即提升变量的可见度,和生命周期)是闭包同一特性的一体两面。只要使用闭包,内存泄漏的风险是一定存在的,这是由于闭包的天然特性所决定。如定义,闭包是该元素以及它相关的上下文元素在一起;只要改元素还在栈上,那么它的相关上下文也就还在被引用着,因为没法被回收;下面举几个例子;通过几个例子,也顺便理解一下lamdba & inner class的异同,以及简单了解一下编译器编译实现它们的一些机制细节;
- 当闭包内部没有引用任何外部变量时;lambda 和 inner class表现不同;
public class MomeryLeak {
Integer[] bigdata = new Integer[1024 * 1024 * 100]; // 400m
Integer[] bigdata2 = new Integer[1024 * 1024 * 100]; // 400m
public Consumer<Void> createArrays() {
// 当使用lambda表达式时,在运行完gc以后, 空间可以正常回收;
Consumer<Void> f = (a)->{System.out.println("hello");};
//当使用内部类时, 在运行完gc以后,空间依然占用;可见lambda并非内部类的语法糖;
/*Consumer<Void> f = new Consumer<Void>(){
@Override
public void accept(Void t) {
System.out.println("hello");
}
};*/
return f;
}
public static void main(String[] args) {
MomeryLeak ml = new MomeryLeak();
Consumer<Void> f = ml.createArrays();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
ml = null;
System.gc();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
f.accept(null);
}
}
- 当闭包内引用外部变量时,lambda 和 inner class表现一致;注意,其实我只引用了其中一个变量bigdata, 而另一个变量bigdata2的空间也无法回收;
public class MomeryLeak {
Integer[] bigdata = new Integer[1024 * 1024 * 100]; // 400m
Integer[] bigdata2 = new Integer[1024 * 1024 * 100]; // 400m
public Consumer<Void> createArrays() {
//Consumer<Void> f = (a)->{System.out.println(bigdata);};
Consumer<Void> f = new Consumer<Void>(){
@Override
public void accept(Void t) {
System.out.println(bigdata);
}
};
return f;
}
public static void main(String[] args) {
MomeryLeak ml = new MomeryLeak();
Consumer<Void> f = ml.createArrays();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
ml = null;
System.gc();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
f.accept(null);
}
}
- 我稍微修改了代码 ml = null -> ml.bigdata2 = null; 无论时lamdba 还是 inner class 相关的空间都可以被正常回收;即,无论时lamdba 还是 inner class, 它持有的外部对象本身引用的copy(outter this copy), 而并非直接是成员变量引用的copy;
public class MomeryLeak {
Integer[] bigdata = new Integer[1024 * 1024 * 100]; // 400m
Integer[] bigdata2 = new Integer[1024 * 1024 * 100]; // 400m
public Consumer<Void> createArrays() {
Consumer<Void> f = (a)->{System.out.println(bigdata);};
/*Consumer<Void> f = new Consumer<Void>(){
@Override
public void accept(Void t) {
System.out.println(bigdata);
}
};*/
return f;
}
public static void main(String[] args) {
MomeryLeak ml = new MomeryLeak();
Consumer<Void> f = ml.createArrays();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
//ml = null;
ml.bigdata = null;
System.gc();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
f.accept(null);
}
}
- 当时局部变量时,lamdba与inner class是一致的,只有被使用的那个变量才会被占用;inner class反编译出来,你会发现这个局部变量 也会作为被传入构造体;
public Consumer<Void> createArrays() {
Integer[] bigdata = new Integer[1024 * 1024 * 100]; // 400m
Integer[] bigdata2 = new Integer[1024 * 1024 * 100]; // 400m
//Consumer<Void> f = (a)->{System.out.println(bigdata); System.out.println(bigdata2);};
Consumer<Void> f = new Consumer<Void>(){
@Override
public void accept(Void t) {
System.out.println(bigdata);
}
};
return f;
}
public static void main(String[] args) {
MomeryLeak ml = new MomeryLeak();
Consumer<Void> f = ml.createArrays();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
ml = null;
// ml.bigdata = null;
System.gc();
System.out.println("Used Memory:"
+ (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
f.accept(null);
}
}