1.首先,什么是闭包
先看这么一个图:
G,F,N 分别代表三个层次的函数,a,b,c分别是其中的变量。正常情况下,G无法访问到N中的c。但是,通过闭包G可以访问到c。这是一般对闭包的初体验,下面看看维基对闭包的定义。
维基的定义:在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
看起来意见还不统一,总结一下:
1.闭包是一个函数,它引用了自由变量并和这个函数包在一起。
2.闭包不是函数,他是函数和引用环境组成的实体。
这两个定义其实差不多,只是在纠结闭包是否是一个函数。第二种较为全面,理由是函数是一些可执行的代码,这些代码在编写时就定义了并且只有一个实例,但是闭包会因为引用的变量不同而产生不同的实例。因此闭包和函数还是有差别的,它只是像函数,但并不是函数。
以python为例说明闭包有多个实例:
这个例子里,函数make_add返回一个lambda,lambda可以通过不同的参数来生成不同的闭包实例,其中的参数就是引用变量,当然引用变量不止这些,还包括所有能引用到的变量,这些引用变量被称为上值(upvalue)。
构成闭包其实只要有两点:
1.函数的代码逻辑
2.引用环境
引用环境是什么呢?
引用环境是一组变量的定义
identifer----value
identifer----value
identifer----value
.......
以上就是一个引用环境,词法作用域的语言中,在函数取值时,会捕捉函数定义时能引用到的所有变量,将其组成一个新的引用环境。和这个函数一起,就组成了闭包。由于每种语言实现引用环境有所不同,对闭包的理解便有所不同。在回到开始这个例子,N为了维持它的词法域,将他所有能访问到的局部变量都包含在了一起。
2.闭包的条件
从上面的定义可以得知
1.函数可以嵌套定义
2.可以定义匿名函数
3.upvalue
4.函数和upvalue可以组成一个可调用的实体。
5.函数是一种数据类型,可以赋值,可以作为参数,也可以做为返回值。
3.Java中的闭包
从上面的闭包条件可知,闭包几乎是函数式编程的专利,Java作为强制的面向对象编程语言怎么可能有闭包,先不管!看看Java是如何实现闭包的。
a.语法
首先要声明一个函数式接口,比如Runnable:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
函数式接口只允许有一个函数,如果你有一个接口,并且这个接口只有一个方法,即便你没有@FunctionalInterface,编译器也会认为这是一个函数接口。但是建议还是@FunctionalInterface一下,如果其他人在这个接口下添加方法,IDE会告诉他:你这么做是非法的。
有了这个函数式接口就可以用Lambda表达式了,格式如下
(参数) -> 表达式
如果没有参数也可以简化为(),如果只有一个参数,可以不要括号
表达式需要用{}块起来,如果只有一行代码则不需要。
比如:
new Thread(() -> System.out.println("Hello world!"));
1. () -> 5 // return都不要写
2. x -> 2 * x // 括号都可以不要
3. (x, y) -> x – y // 参数声明也可以免写
4. (int x, int y) -> x + y // 你也可以声明
5. (String s) -> System.out.print(s) // 大括号也懒得打了
还可以用::两个冒号来简写
1.System.out::println //非静态方法引用
2.String::valueOf //静态方法引用
3.ArrayList::new //构造函数的引用
b.特性
特性1:不允许修改局部变量
比如以下写法是不允许的
int n= 1;
new Thread(() -> n++);
提示:Local variable n defined in an enclosing scope must be final or effectively final
把n改成final就可以了,如果你不修改局部变量的值并不是需要final。如果没有局部变量的捕获,据说这样的闭包要高效一些。
特性2:参数也必须是final
特性3:可以访问并修改外部类的变量
特性4:不能将异常抛出
惊!看起来就和内部类一样!
c.分析
函数式编程中,函数是第一公民,可以被嵌套定义,也可以被当作参数使用,就像面向对象编程的类一样。再联系上文提到的闭包条件,我们发现Java作为一种强制面向对象的语言,几乎是不能有闭包的,但是如果将函数改为类,那么情况就不一样了。事实上Java 8也就是这么干的。
函数式接口规定只能有一个方法,目的在于使这个类的作用就像一个函数一样。仅此一步,闭包的条件就满足了。
d.思考
1.和内部类的异同
异:
不会生成新的类
this不同
同:
不能改变局部变量的值
可以改变全局变量的值
2.既然匿名内部类和闭包那么相同,那么内部类也可以算是一个闭包吗?
有种观点认为,匿名内部类是面向对象的闭包,理由是内部类不仅包含了代码逻辑还包含了外围类的引用,符合闭包的定义。Oracle官网也提到:
Lambda expressions to replace anonymous inner classes in Java SE 8.
他就是为了解决匿名类的。
另一个佐证:在Java还没支持闭包之前,就有人提出修改匿名内部类的创建方式来实现闭包。
3又是一个语法糖?
并不是,同样一个接口,用内部类实现,会多出一个class文件来,如果用闭包则不会。
4.闭包的优缺点
闭包有更加简洁的语法,更清晰的逻辑,也更加抽象。但是闭包的内存开销会大一些,也不要滥用。
e.应用
Java8的Function包有一大堆的函数式接口,以供使用
http://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
如forEach,你现在不需要写一个函数,你只需要这样做就可以了
list.forEach(System.out::println);
方法的实现:
//Consumer是function包下的一个函数式接口。
default void forEach(Consumer action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Stream是是目前Java Lamdba表达式的重头戏,Stream并不是IO流,而是对集合集合操作的迭代,普通的Iterator只是对集合元素一个一个的遍历,而Stream不仅实现了遍历,还包含了对元素操作的描述,如过滤,内类转换等。这些描述就是Lamdba表达式。
下面看一个官网的示例:
int sum = widgets.stream()
.filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
上例中,我们需要获取widgets集合中颜色为红色的元素,并将宽度转换为Int再求和,这个例子看起来就像我们使用AlertDialog.Builder创建Dialog一样,每个setXX方法都返回Builder对象,最终通过create方法创建真正的Dialog。
Stream有两种形式:
1.Intermediate
Intermediate是中间操作,其后可以再跟Stream操作,如上例的filter,就像Builder一样。辨别Intermediate可以通过方法是否返回Stream来辨别,Intermediate的操作不会执行真正的遍历,它的目的是打开流,并包含了这样的操作,再返回新的流交给下一个操作使用。
2.Terminal
最终操作,其后不能再跟Stream操作,就像Create一样。Terminal操作返回值不是Stream,可能是一个数据类型或者Void,如上例的sum,返回的值是Int。因此Terminal是真正执行遍历的地方。
Strem的特性:
1.没有存储,流不是一个数据结构,是一个数据操作的管道(through a pipeline of computational operations.)。每一次值转换都生成一个新的Stream对象,因此操作可以向链条一样形成一个管道。
流的实现类是AbstractPipeline,其主要的属性有:
private Spliterator sourceSpliterator;//资源的迭代器,包含了集合
private final AbstractPipeline sourceStage;//自身Stream
private final AbstractPipeline previousStage;//上一个Stream
private AbstractPipeline nextStage;//下一个Stream
2.不修改源数据
private final Collection collection; // null OK
3.无界操作,即Short-circuiting,他的作用是在无限的集合中返回有限的流或者在有限的时间内完成操作。这一类的方法有anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit。
4.中间操作是懒惰的
5.只能执行一次最终操作
6.并行,采用fork/join框架
list.parallelStream();