1.首先仓蛆,什么是閉包
先看這么一個(gè)圖:
G,F,N 分別代表三個(gè)層次的函數(shù)笤闯,a,b,c分別是其中的變量。正常情況下阁猜,G無法訪問到N中的c聘惦。但是琳彩,通過閉包G可以訪問到c。這是一般對(duì)閉包的初體驗(yàn)部凑,下面看看維基對(duì)閉包的定義。
維基的定義:在計(jì)算機(jī)科學(xué)中碧浊,閉包(英語:Closure)涂邀,又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures),是引用了自由變量的函數(shù)箱锐。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在比勉,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。所以驹止,有另一種說法認(rèn)為閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體浩聋。閉包在運(yùn)行時(shí)可以有多個(gè)實(shí)例,不同的引用環(huán)境和相同的函數(shù)組合可以產(chǎn)生不同的實(shí)例臊恋。
看起來意見還不統(tǒng)一衣洁,總結(jié)一下:
1.閉包是一個(gè)函數(shù),它引用了自由變量并和這個(gè)函數(shù)包在一起抖仅。
2.閉包不是函數(shù)坊夫,他是函數(shù)和引用環(huán)境組成的實(shí)體砖第。
這兩個(gè)定義其實(shí)差不多,只是在糾結(jié)閉包是否是一個(gè)函數(shù)环凿。第二種較為全面梧兼,理由是函數(shù)是一些可執(zhí)行的代碼,這些代碼在編寫時(shí)就定義了并且只有一個(gè)實(shí)例智听,但是閉包會(huì)因?yàn)橐玫淖兞坎煌a(chǎn)生不同的實(shí)例羽杰。因此閉包和函數(shù)還是有差別的,它只是像函數(shù)到推,但并不是函數(shù)考赛。
以python為例說明閉包有多個(gè)實(shí)例:
這個(gè)例子里,函數(shù)make_add返回一個(gè)lambda环肘,lambda可以通過不同的參數(shù)來生成不同的閉包實(shí)例欲虚,其中的參數(shù)就是引用變量,當(dāng)然引用變量不止這些悔雹,還包括所有能引用到的變量复哆,這些引用變量被稱為上值(upvalue)。
構(gòu)成閉包其實(shí)只要有兩點(diǎn):
1.函數(shù)的代碼邏輯
2.引用環(huán)境
引用環(huán)境是什么呢腌零?
引用環(huán)境是一組變量的定義
identifer----value
identifer----value
identifer----value
.......
以上就是一個(gè)引用環(huán)境梯找,詞法作用域的語言中,在函數(shù)取值時(shí)益涧,會(huì)捕捉函數(shù)定義時(shí)能引用到的所有變量锈锤,將其組成一個(gè)新的引用環(huán)境。和這個(gè)函數(shù)一起闲询,就組成了閉包久免。由于每種語言實(shí)現(xiàn)引用環(huán)境有所不同,對(duì)閉包的理解便有所不同扭弧。在回到開始這個(gè)例子阎姥,N為了維持它的詞法域,將他所有能訪問到的局部變量都包含在了一起鸽捻。
2.閉包的條件
從上面的定義可以得知
1.函數(shù)可以嵌套定義
2.可以定義匿名函數(shù)
3.upvalue
4.函數(shù)和upvalue可以組成一個(gè)可調(diào)用的實(shí)體呼巴。
5.函數(shù)是一種數(shù)據(jù)類型,可以賦值御蒲,可以作為參數(shù)衣赶,也可以做為返回值。
3.Java中的閉包
從上面的閉包條件可知厚满,閉包幾乎是函數(shù)式編程的專利府瞄,Java作為強(qiáng)制的面向?qū)ο缶幊陶Z言怎么可能有閉包,先不管碘箍!看看Java是如何實(shí)現(xiàn)閉包的摘能。
a.語法
首先要聲明一個(gè)函數(shù)式接口,比如Runnable:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
函數(shù)式接口只允許有一個(gè)函數(shù)续崖,如果你有一個(gè)接口,并且這個(gè)接口只有一個(gè)方法团搞,即便你沒有@FunctionalInterface严望,編譯器也會(huì)認(rèn)為這是一個(gè)函數(shù)接口。但是建議還是@FunctionalInterface一下逻恐,如果其他人在這個(gè)接口下添加方法像吻,IDE會(huì)告訴他:你這么做是非法的。
有了這個(gè)函數(shù)式接口就可以用Lambda表達(dá)式了复隆,格式如下
(參數(shù)) -> 表達(dá)式
如果沒有參數(shù)也可以簡(jiǎn)化為()拨匆,如果只有一個(gè)參數(shù),可以不要括號(hào)
表達(dá)式需要用{}塊起來挽拂,如果只有一行代碼則不需要惭每。
比如:
new Thread(() -> System.out.println("Hello world!"));
1.? () -> 5? ? ? ? ? ? ? ? ? ? ? ? ? // return都不要寫
2.? x -> 2 * x? ? ? ? ? ? // 括號(hào)都可以不要
3.? (x, y) -> x – y? ? ? ? ? ? ? ? ? ? // 參數(shù)聲明也可以免寫
4.? (int x, int y) -> x + y? ? ? // 你也可以聲明
5.? (String s) -> System.out.print(s) // 大括號(hào)也懶得打了
還可以用::兩個(gè)冒號(hào)來簡(jiǎn)寫
1.System.out::println? ? //非靜態(tài)方法引用
2.String::valueOf? ? //靜態(tài)方法引用
3.ArrayList::new? ? //構(gòu)造函數(shù)的引用
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。如果沒有局部變量的捕獲亏栈,據(jù)說這樣的閉包要高效一些台腥。
特性2:參數(shù)也必須是final
特性3:可以訪問并修改外部類的變量
特性4:不能將異常拋出
驚!看起來就和內(nèi)部類一樣绒北!
c.分析
函數(shù)式編程中黎侈,函數(shù)是第一公民,可以被嵌套定義闷游,也可以被當(dāng)作參數(shù)使用峻汉,就像面向?qū)ο缶幊痰念愐粯印T俾?lián)系上文提到的閉包條件脐往,我們發(fā)現(xiàn)Java作為一種強(qiáng)制面向?qū)ο蟮恼Z言休吠,幾乎是不能有閉包的,但是如果將函數(shù)改為類业簿,那么情況就不一樣了瘤礁。事實(shí)上Java 8也就是這么干的。
函數(shù)式接口規(guī)定只能有一個(gè)方法辖源,目的在于使這個(gè)類的作用就像一個(gè)函數(shù)一樣。僅此一步希太,閉包的條件就滿足了克饶。
d.思考
1.和內(nèi)部類的異同
異:
不會(huì)生成新的類
this不同
同:
不能改變局部變量的值
可以改變?nèi)肿兞康闹?/p>
2.既然匿名內(nèi)部類和閉包那么相同,那么內(nèi)部類也可以算是一個(gè)閉包嗎誊辉?
有種觀點(diǎn)認(rèn)為矾湃,匿名內(nèi)部類是面向?qū)ο蟮拈]包,理由是內(nèi)部類不僅包含了代碼邏輯還包含了外圍類的引用堕澄,符合閉包的定義邀跃。Oracle官網(wǎng)也提到:
Lambda expressions to replace anonymous inner classes in Java SE 8.
他就是為了解決匿名類的霉咨。
另一個(gè)佐證:在Java還沒支持閉包之前,就有人提出修改匿名內(nèi)部類的創(chuàng)建方式來實(shí)現(xiàn)閉包拍屑。
3又是一個(gè)語法糖?
并不是途戒,同樣一個(gè)接口,用內(nèi)部類實(shí)現(xiàn)僵驰,會(huì)多出一個(gè)class文件來喷斋,如果用閉包則不會(huì)。
4.閉包的優(yōu)缺點(diǎn)
閉包有更加簡(jiǎn)潔的語法蒜茴,更清晰的邏輯星爪,也更加抽象。但是閉包的內(nèi)存開銷會(huì)大一些粉私,也不要濫用顽腾。
e.應(yīng)用
Java8的Function包有一大堆的函數(shù)式接口,以供使用
http://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
如forEach,你現(xiàn)在不需要寫一個(gè)函數(shù)诺核,你只需要這樣做就可以了
list.forEach(System.out::println);
方法的實(shí)現(xiàn):
//Consumer是function包下的一個(gè)函數(shù)式接口抄肖。
default void forEach(Consumer action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Stream是是目前Java Lamdba表達(dá)式的重頭戲,Stream并不是IO流猪瞬,而是對(duì)集合集合操作的迭代憎瘸,普通的Iterator只是對(duì)集合元素一個(gè)一個(gè)的遍歷,而Stream不僅實(shí)現(xiàn)了遍歷陈瘦,還包含了對(duì)元素操作的描述幌甘,如過濾,內(nèi)類轉(zhuǎn)換等痊项。這些描述就是Lamdba表達(dá)式锅风。
下面看一個(gè)官網(wǎng)的示例:
int sum = widgets.stream()
.filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
上例中,我們需要獲取widgets集合中顏色為紅色的元素鞍泉,并將寬度轉(zhuǎn)換為Int再求和皱埠,這個(gè)例子看起來就像我們使用AlertDialog.Builder創(chuàng)建Dialog一樣,每個(gè)setXX方法都返回Builder對(duì)象咖驮,最終通過create方法創(chuàng)建真正的Dialog边器。
Stream有兩種形式:
1.Intermediate
Intermediate是中間操作,其后可以再跟Stream操作托修,如上例的filter忘巧,就像Builder一樣。辨別Intermediate可以通過方法是否返回Stream來辨別睦刃,Intermediate的操作不會(huì)執(zhí)行真正的遍歷砚嘴,它的目的是打開流,并包含了這樣的操作,再返回新的流交給下一個(gè)操作使用际长。
2.Terminal
最終操作耸采,其后不能再跟Stream操作,就像Create一樣工育。Terminal操作返回值不是Stream虾宇,可能是一個(gè)數(shù)據(jù)類型或者Void,如上例的sum,返回的值是Int翅娶。因此Terminal是真正執(zhí)行遍歷的地方文留。
Strem的特性:
1.沒有存儲(chǔ),流不是一個(gè)數(shù)據(jù)結(jié)構(gòu)竭沫,是一個(gè)數(shù)據(jù)操作的管道(through a pipeline of computational operations.)燥翅。每一次值轉(zhuǎn)換都生成一個(gè)新的Stream對(duì)象,因此操作可以向鏈條一樣形成一個(gè)管道蜕提。
流的實(shí)現(xiàn)類是AbstractPipeline森书,其主要的屬性有:
private Spliterator sourceSpliterator;//資源的迭代器,包含了集合
private final AbstractPipeline sourceStage;//自身Stream
private final AbstractPipeline previousStage;//上一個(gè)Stream
private AbstractPipeline nextStage;//下一個(gè)Stream
2.不修改源數(shù)據(jù)
private final Collection collection; // null OK
3.無界操作,即Short-circuiting谎势,他的作用是在無限的集合中返回有限的流或者在有限的時(shí)間內(nèi)完成操作凛膏。這一類的方法有anyMatch、 allMatch脏榆、 noneMatch猖毫、 findFirst、 findAny须喂、 limit吁断。
4.中間操作是懶惰的
5.只能執(zhí)行一次最終操作
6.并行,采用fork/join框架
list.parallelStream();