什么是闭包
闭包的英文对应的是Closure,如果要单纯的讨论这个概念的话就要提到和图灵机起名的大名鼎鼎的lambda演算(lamdba calculus)。尽管lamdba的概念并不是本文的重点,但是闭包概念的目的便是支持lamdba的实现。如果你单独地在百度对进行搜索闭包的话,你会发现大部分都是js相关的内容,主要是js本身就只用闭包的这个概念。但是闭包并不仅限于js,而是一个通用的概念。借用wiki中有点抽象的定义来说的话,闭包就是:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。
简单来说就是当一个方法引用了方法局部变量外的变量时,它就是一个闭包。而如果根据这个定义继续延展的话,就可以得到另外的一种描述方法:
闭包是由函数和与其相关的引用环境(方法外变量)组合而成的实体。
为什么需要闭包
尽管给出了两种关于闭包是什么的定义,但是闭包本身的概念还是较为的抽象。比如直接给例子,我们可以先讨论一下为什么需要闭包。
为了理解闭包,我们可以先假设这样的一种场景:
我有一个方法A,其中有私有变量。我想在别的方法中可以直接访问并使用它,但是又不想直接暴露它。
这时候,就可以在A建立一个内部类B来访问私有变量。那么这时候,这个内部类中你所可以访问B的方法,就和A中的私有变量形成了闭包。此时B已经捕获了A中的变量。即便是A对象销毁了,被B捕获的私有变量仍然不会释放。
所以可以理解,如果希望系统方法和当前环境的上下文绑定的话,就可以使用闭包。
尽管有些不恰当,但是我们可以再换一个实际的场景来举个例子帮助你理解:
当你在购物的时候,你选中了一件商品,然后下单。这时候你的订单就已经绑定了你选中的商品。那么你的购买行为就和商品组成了闭包。这时候即便商家下架商品,也不会影响到商品后续的发货签收。并且这些逻辑只需要根据购买行为中的信息进行判断就可以了,而不用关心商家什么时候把商品下架,这写逻辑发生时候的商品价格是什么。
结合上面的例子,我们可以发现,当一个方法和其调用的外部环境形成闭包的时候,由于外部环境已经确定,那么我们就不用再关心外部环境了,而只用关心方法本身。
所以针对我们为什么需要闭包,我给出的答案是:
使用闭包的设计方式,由于闭包本身已经包含了上下文信息,所以可以对北向功能调用(用户)屏蔽由于环境而引发的复杂处理和交互成本。
Java中的闭包
对于Java来说,可以理解为主要是两种的闭包方式:
其中内部类除了本身的内部类还有局部内部类、匿名内部类。我们以最简洁的匿名内部类来举例:
public class Closure {
private int length = 0;
public ISay count() {
return new ISay() {
@Override
public void say(String message) {
length = message.length();
return "Talk:" + length;
}
};
}
}
此时say方法中便捕获了length属性,而如果你使用的是足够版本的IDE的话,获取还会提示你:
Anonymous new ISay() can be replaced with lambda
替换后:
public class Closure {
private int length = 0;
public ISay count() {
return message -> {
length = message.length();
return "Talk:" + length;
};
}
}
那么这时候你就会发现,此时return的对象就是一个通过匿名后直接描述的函数,而这个函数同时还关联了上下文之外的环境信息。
而在java8中lambda的中我们可以通过函数式编程接口直接接收这种闭包,这些接口常用的为:
- Supplier
- Predicate
- Function
关于函数式接口本文就不展开了,但是利用函数式接口,我们就可以这样传递一个闭包:
public class Closure {
private int length = 0;
public Function say() {
return message -> {
length = message.length();
return "Talk:" + length;
};
}
}
此时say返回的就不是String的结果了,而是直接将闭包本身返回了。而这个闭包在创建的时候就已经绑定了所需要的环境属性。所以我们可以在需要的时候再调用闭包,并且不用再关心它到底依赖了其他哪些变量:
public static void main(String[] args) {
final Function say = new Closure().say();
System.out.println(say.apply("Something."));
}
当然你可能会角色,这个length似乎没有什么用呀,那如果我们换一个形式:
@Service
public class Closure {
@Resource
private HowtoSay howToSay;
public Function say() {
return message -> {
return "Talk:" + howToSay.say(message);
};
}
}
使用Spring的依赖注入后,那么这个Closure类本身可能是各种策略模式策略器中的一个,策略器返回的是一个已经关联了具体策略路由的闭包。而当这个方法提供出去的时候,后续的调用者只需要知道这个闭包是可以针对文本进行处理的就可以,而至于之前是使用的什么策略它则不用关心。因为这些细节都已经通过闭包屏蔽了。
闭包的问题
仅仅谈论好的而对问题闭口不谈确实不好,虽然闭包提供了强大的功能,可以对业务细节进行屏蔽,对系统进行接耦拆分。但是闭包本身确实有一些问题需要留意:
可以发现,这两个问题都是由于闭包本身的优点而产生的。由于闭包关联了环境信息,所以其让环境信息中对象的生命周期变长,这对于系统性能的维护以及jvm的垃圾回收都有负面因素。而同时因为不同于一般的编码风格,闭包的使用需要开发人员对实体进行抽象,才能比较好地实现。总结来说,对于开发人员本身有一定要求。
加餐
平时我们也经常使用lambda表达式来处理一些业务逻辑,偶尔会出现一下的情况:
interface Action
{
String doAction(String id);
}
public List doAction(List userIds){
List result = new ArrayList<>();
for (int i = 0; i < 10; i++) {
result.add( id -> userIds.get(i)+"#");
}
return result;
}
先不管这段代码的实现业务背景是什么,但是IDE会提示在userIds.get(i)中的i提示的信息为:
Variable used in lambda expression should be final or effectively final
结合了上文中关于闭包的内容,我们就不难理解。由于闭包是要关联外部环境变量,在这部分代码中关联的是索引变量i,但是因为i本身是局部变量,无法保证关联环境的稳定性(我自己的理解),所以java编译器会强制的要求当闭包关联的是局部变量的时候,需要添加final关键字,而在进行final关键字从而保证该变量的内存引用不会发生改变。从本章的代码上结论上看则是进行了一次内存拷贝,来保证每个闭包中关联的环境变量不会改变,修改后的代码为:
public List doAction(List userIds){
List result = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int finalI = i;
result.add(id -> userIds.get(finalI)+"#");
}
return result;
}
最后
闭包本身是一种面向抽象编程,屏蔽细节的设计原则。在良好的设计下,可以通过闭包来屏蔽对于环境信息的感知,从而简化外部对于系统理解的成本,提高系统的易用性。
关键词:闭包是啥概念(闭包的概念)