原文?http://www.codeceo.com/article/learn-java-lambda.html
Lambda簡介
Lambda作為函數(shù)式編程中的基礎(chǔ)部分,在其他編程語言(例如:Scala)中早就廣為使用,但在Java領(lǐng)域中發(fā)展較慢,直到j(luò)ava8,才開始支持Lambda。
拋開數(shù)學(xué)定義不看,直接來認(rèn)識Lambda。Lambda表達(dá)式本質(zhì)上是匿名方法,其底層還是通過invokedynamic指令來生成匿名類來實現(xiàn)。它提供了更為簡單的語法和寫作方式,允許你通過表達(dá)式來代替函數(shù)式接口。在一些人看來,Lambda就是可以讓你的代碼變得更簡潔,完全可以不使用——這種看法當(dāng)然沒問題,但重要的是lambda為Java帶來了閉包。得益于Lamdba對集合的支持,通過Lambda在多核處理器條件下對集合遍歷時的性能提高極大,另外我們可以以數(shù)據(jù)流的方式處理集合——這是非常有吸引力的。
Lambda的語法極為簡單,類似如下結(jié)構(gòu):
(parameters) -> expression
或者
(parameters) -> { statements; }
Lambda表達(dá)式由三部分組成:
paramaters:類似方法中的形參列表,這里的參數(shù)是函數(shù)式接口里的參數(shù)。這里的參數(shù)類型可以明確的聲明也可不聲明而由JVM隱含的推斷。另外當(dāng)只有一個推斷類型時可以省略掉圓括號。
->:可理解為“被用于”的意思
方法體:可以是表達(dá)式也可以代碼塊,是函數(shù)式接口里方法的實現(xiàn)。代碼塊可返回一個值或者什么都不反回,這里的代碼塊塊等同于方法的方法體。如果是表達(dá)式,也可以返回一個值或者什么都不反回。
我們通過以下幾個示例來做說明:
//示例1:不需要接受參數(shù),直接返回10()->10//示例2:接受兩個int類型的參數(shù),并返回這兩個參數(shù)相加的和(intx,inty)->x+y;//示例2:接受x,y兩個參數(shù),該參數(shù)的類型由JVM根據(jù)上下文推斷出來,并返回兩個參數(shù)的和(x,y)->x+y;//示例3:接受一個字符串,并將該字符串打印到控制到,不反回結(jié)果(String name)->System.out.println(name);//示例4:接受一個推斷類型的參數(shù)name,并將該字符串打印到控制臺name->System.out.println(name);//示例5:接受兩個String類型參數(shù),并分別輸出,不反回(String name,String sex)->{System.out.println(name);System.out.println(sex)}//示例6:接受一個參數(shù)x,并返回該該參數(shù)的兩倍x->2*x
Lambda用在哪里
在[函數(shù)式接口][1]中我們知道Lambda表達(dá)式的目標(biāo)類型是函數(shù)性接口——每一個Lambda都能通過一個特定的函數(shù)式接口與一個給定的類型進(jìn)行匹配。因此一個Lambda表達(dá)式能被應(yīng)用在與其目標(biāo)類型匹配的任何地方,lambda表達(dá)式必須和函數(shù)式接口的抽象函數(shù)描述一樣的參數(shù)類型,它的返回類型也必須和抽象函數(shù)的返回類型兼容,并且他能拋出的異常也僅限于在函數(shù)的描述范圍中。
接下來,我們看一個自定義的函數(shù)式接口示例:
@FunctionalInterfaceinterfaceConverter{Tconvert(F from);}
首先用傳統(tǒng)的方式來使用該接口:
Converter converter=newConverter() {? ? ? ? ? ? @Override? ? ? ? ? ? public Integer convert(Stringfrom) {returnInteger.valueOf(from);? ? ? ? ? ? }? ? ? ? };? ? ? Integer result = converter.convert("200");? ? ? ? System.out.println(result);
很顯然這沒任何問題,那么接下里就是Lambda上場的時刻,用Lambda實現(xiàn)Converter接口:
Converter converter=(param)->Integer.valueOf(param);? ? ? ? Integer result = converter.convert("101");? ? ? ? System.out.println(result);
通過上例,我想你已經(jīng)對Lambda的使用有了個簡單的認(rèn)識,下面,我們在用一個常用的Runnable做演示:
在以前我們可能會寫下這種代碼:
newThread(newRunnable() {@Overridepublicvoidrun(){? ? ? ? ? ? ? ? System.out.println("hello lambda");? ? ? ? ? ? }? ? ? ? }).start();
在某些情況下,大量的匿名類會讓代碼顯得雜亂無章?,F(xiàn)在可以用Lambda來使它變得簡潔:
newThread(()->System.out.println("hello lambda")).start();
方法引用
方法引用是Lambda表達(dá)式的一個簡化寫法。所引用的方法其實是Lambda表達(dá)式的方法體的實現(xiàn),其語法結(jié)構(gòu)為:
ObjectRef::methodName
左邊可以是類名或者實例名,中間是方法引用符號”::”,右邊是相應(yīng)的方法名。方法引用被分為三類:
在某些情況下,我們可能寫出這樣的代碼:
publicclassReferenceTest{publicstaticvoidmain(String[] args){? ? ? ? Converter converter=newConverter() {@OverridepublicIntegerconvert(String from){returnReferenceTest.String2Int(from);? ? ? ? ? ? }? ? ? ? };? ? ? ? converter.convert("120");? ? }@FunctionalInterfaceinterfaceConverter{Tconvert(F from);? ? }staticintString2Int(String from){returnInteger.valueOf(from);? ? }}
這時候如果用靜態(tài)引用會使的代碼更加簡潔:
Converter converter = ReferenceTest::String2Int; converter.convert("120");
2. 實例方法引用
我們也可能會寫下這樣的代碼:
publicclassReferenceTest{publicstaticvoidmain(String[] args){? ? ? ? Converter converter =newConverter() {@OverridepublicIntegerconvert(String from){returnnewHelper().String2Int(from);? ? ? ? ? ? }? ? ? ? };? ? ? ? converter.convert("120");? ? }@FunctionalInterfaceinterfaceConverter{Tconvert(F from);? ? }staticclassHelper{publicintString2Int(String from){returnInteger.valueOf(from);? ? ? ? }? ? }}
同樣用實例方法引用會顯得更加簡潔:
Helper helper =newHelper();? Converter converter = helper::String2Int;? converter.convert("120");
3. 構(gòu)造方法引用
現(xiàn)在我們來演示構(gòu)造方法的引用。首先我們定義一個父類Animal:
classAnimal{privateString name;privateintage;publicAnimal(String name,intage){this.name = name;this.age = age;? ? ? ? }publicvoidbehavior(){? ? ? ? }? ? }
接下來,我們在定義兩個Animal的子類:Dog、Bird
publicclassBirdextendsAnimal{publicBird(String name,intage){super(name, age);? ? }@Overridepublicvoidbehavior(){? ? ? ? System.out.println("fly");? ? }}classDogextendsAnimal{publicDog(String name,intage){super(name, age);? ? }@Overridepublicvoidbehavior(){? ? ? ? System.out.println("run");? ? }}
隨后我們定義工廠接口:
interfaceFactory{Tcreate(String name,intage);? ? }
接下來我們還是用傳統(tǒng)的方法來創(chuàng)建Dog類和Bird類的對象:
Factory factory=newFactory() {@OverridepublicAnimalcreate(String name,intage){returnnewDog(name,age);? ? ? ? ? ? }? ? ? ? };? ? ? ? factory.create("alias",3);? ? ? ? factory=newFactory() {@OverridepublicAnimalcreate(String name,intage){returnnewBird(name,age);? ? ? ? ? ? }? ? ? ? };? ? ? ? factory.create("smook",2);
僅僅為了創(chuàng)建兩個對象就寫了十多號代碼,現(xiàn)在我們用構(gòu)造函數(shù)引用試試:
Factory dogFactory =Dog::new;? Animal dog = dogFactory.create("alias",4);? Factory birdFactory = Bird::new;? Bird bird = birdFactory.create("smook",3);
這樣代碼就顯得干凈利落了。通過Dog::new這種方式來穿件對象時,F(xiàn)actory.create函數(shù)的簽名選擇相應(yīng)的造函數(shù)。
域即作用域,Lambda表達(dá)式中的參數(shù)列表中的參數(shù)在該Lambda表達(dá)式范圍內(nèi)(域)有效。在作用Lambda表達(dá)式內(nèi),可以訪問外部的變量:局部變量、類變量和靜態(tài)變量,但操作受限程度不一。
在Lambda表達(dá)式外部的局部變量會被JVM隱式的編譯成final類型,因此只能訪問外而不能修改。
publicclassReferenceTest{publicstaticvoidmain(String[] args){intn =3;? ? ? ? Calculate calculate = param -> {//n=10; 編譯錯誤returnn + param;? ? ? ? };? ? ? ? calculate.calculate(10);? ? }@FunctionalInterfaceinterfaceCalculate{intcalculate(intvalue);? ? }}
訪問靜態(tài)變量和成員變量
在Lambda表達(dá)式內(nèi)部,對靜態(tài)變量和成員變量可讀可寫。
publicclassReferenceTest{publicintcount =1;publicstaticintnum =2;publicvoidtest(){? ? ? ? Calculate calculate = param -> {? ? ? ? ? ? num =10;//修改靜態(tài)變量count =3;//修改成員變量returnn + param;? ? ? ? };? ? ? ? calculate.calculate(10);? ? }publicstaticvoidmain(String[] args){? ? }@FunctionalInterfaceinterfaceCalculate{intcalculate(intvalue);? ? }}
Lambda不能訪問函數(shù)接口的默認(rèn)方法
java8增強(qiáng)了接口,其中包括接口可添加default關(guān)鍵詞定義的默認(rèn)方法,這里我們需要注意,Lambda表達(dá)式內(nèi)部不支持訪問默認(rèn)方法。
在[函數(shù)式接口][2]一節(jié)中,我們提到j(luò)ava.util.function包中內(nèi)置許多函數(shù)式接口,現(xiàn)在將對常用的函數(shù)式接口做說明。
輸入一個參數(shù),并返回一個Boolean值,其中內(nèi)置許多用于邏輯判斷的默認(rèn)方法:
@Test? ? public voidpredicateTest() {? ? ? ? Predicate predicate = (s) -> s.length() > 0;? ? ? ? booleantest= predicate.test("test");? ? ? ? System.out.println("字符串長度大于0:"+test);test= predicate.test("");? ? ? ? System.out.println("字符串長度大于0:"+test);test= predicate.negate().test("");? ? ? ? System.out.println("字符串長度小于0:"+test);? ? ? ? Predicate pre = Objects::nonNull;? ? ? ? Object ob = null;test= pre.test(ob);? ? ? ? System.out.println("對象不為空:"+test);? ? ? ? ob = new Object();test= pre.test(ob);? ? ? ? System.out.println("對象不為空:"+test);? ? }
Function接口
接收一個參數(shù),返回單一的結(jié)果,默認(rèn)的方法(andThen)可將多個函數(shù)串在一起,形成復(fù)合Funtion(有輸入,有輸出)結(jié)果,
@Testpublicvoid functionTest() {FunctiontoInteger=Integer::valueOf;//toInteger的執(zhí)行結(jié)果作為第二個backToString的輸入FunctionbackToString=toInteger.andThen(String::valueOf);? ? ? ? String result = backToString.apply("1234");? ? ? ? System.out.println(result);Functionadd=(i)->{? ? ? ? ? ? System.out.println("frist input:"+ i);returni *2;? ? ? ? };Functionzero=add.andThen((i)-> {? ? ? ? ? ? System.out.println("second input:"+ i);? ? ? ? ? ? return i *0;? ? ? ? });? ? ? ? Integer res = zero.apply(8);? ? ? ? System.out.println(res);? ? }
Supplier接口
返回一個給定類型的結(jié)果,與Function不同的是,Supplier不需要接受參數(shù)(供應(yīng)者,有輸出無輸入)
@TestpublicvoidsupplierTest(){? ? ? ? Supplier supplier = () ->"special type value";? ? ? ? String s = supplier.get();? ? ? ? System.out.println(s);? ? }
Consumer接口
代表了在單一的輸入?yún)?shù)上需要進(jìn)行的操作。和Function不同的是,Consumer沒有返回值(消費者,有輸入,無輸出)
@TestpublicvoidconsumerTest(){? ? ? ? Consumer add5 = (p) -> {? ? ? ? ? ? System.out.println("old value:"+ p);? ? ? ? ? ? p = p +5;? ? ? ? ? ? System.out.println("new value:"+ p);? ? ? ? };? ? ? ? add5.accept(10);? ? }
以上四個接口的用法代表了java.util.function包中四種類型,理解這四個函數(shù)式接口之后,其他的接口也就容易理解了,現(xiàn)在我們來做一下簡單的總結(jié):
Predicate用來邏輯判斷,F(xiàn)unction用在有輸入有輸出的地方,Supplier用在無輸入,有輸出的地方,而Consumer用在有輸入,無輸出的地方。你大可通過其名稱的含義來獲知其使用場景。
Lambda為java8帶了閉包,這一特性在集合操作中尤為重要:java8中支持對集合對象的stream進(jìn)行函數(shù)式操作,此外,stream api也被集成進(jìn)了collection api,允許對集合對象進(jìn)行批量操作。
下面我們來認(rèn)識Stream。
Stream表示數(shù)據(jù)流,它沒有數(shù)據(jù)結(jié)構(gòu),本身也不存儲元素,其操作也不會改變源Stream,而是生成新Stream.作為一種操作數(shù)據(jù)的接口,它提供了過濾、排序、映射、規(guī)約等多種操作方法,這些方法按照返回類型被分為兩類:凡是返回Stream類型的方法,稱之為中間方法(中間操作),其余的都是完結(jié)方法(完結(jié)操作)。完結(jié)方法返回一個某種類型的值,而中間方法則返回新的Stream。中間方法的調(diào)用通常是鏈?zhǔn)降?,該過程會形成一個管道,當(dāng)完結(jié)方法被調(diào)用時會導(dǎo)致立即從管道中消費值,這里我們要記?。篠tream的操作盡可能以“延遲”的方式運行,也就是我們常說的“懶操作”,這樣有助于減少資源占用,提高性能。對于所有的中間操作(除sorted外)都是運行在延遲模式下。
Stream不但提供了強(qiáng)大的數(shù)據(jù)操作能力,更重要的是Stream既支持串行也支持并行,并行使得Stream在多核處理器上有著更好的性能。
Stream的使用過程有著固定的模式:
創(chuàng)建Stream
通過中間操作,對原始Stream進(jìn)行“變化”并生成新的Stream
使用完結(jié)操作,生成最終結(jié)果
也就是
創(chuàng)建——>變化——>完結(jié)
對于集合來說,可以通過調(diào)用集合的stream()或者parallelStream()來創(chuàng)建,另外這兩個方法也在Collection接口中實現(xiàn)了。對于數(shù)組來說,可以通過Stream的靜態(tài)方法of(T … values)來創(chuàng)建,另外,Arrays也提供了有關(guān)stream的支持。
除了以上基于集合或者數(shù)組來創(chuàng)建Stream,也可以通過Steam.empty()創(chuàng)建空的Stream,或者利用Stream的generate()來創(chuàng)建無窮的Stream。
下面我們以串行Stream為例,分別說明Stream幾種常用的中間方法和完結(jié)方法。首先創(chuàng)建一個List集合:
List lists=newArrayList();? ? ? ? lists.add("a1");? ? ? ? lists.add("a2");? ? ? ? lists.add("b1");? ? ? ? lists.add("b2");? ? ? ? lists.add("b3");? ? ? ? lists.add("o1");
中間方法
結(jié)合Predicate接口,F(xiàn)ilter對流對象中的所有元素進(jìn)行過濾,該操作是一個中間操作,這意味著你可以在操作返回結(jié)果的基礎(chǔ)上進(jìn)行其他操作。
publicstaticvoid streamFilterTest() {? ? ? ? lists.stream().filter((s -> s.startsWith("a"))).forEach(System.out::println);//等價于以上操作Predicate predicate = (s) -> s.startsWith("a");? ? ? ? lists.stream().filter(predicate).forEach(System.out::println);//連續(xù)過濾Predicate predicate1 = (s -> s.endsWith("1"));? ? ? ? lists.stream().filter(predicate).filter(predicate1).forEach(System.out::println);? ? }
排序(Sorted)
結(jié)合Comparator接口,該操作返回一個排序過后的流的視圖,原始流的順序不會改變。通過Comparator來指定排序規(guī)則,默認(rèn)是按照自然順序排序。
publicstaticvoid streamSortedTest() {System.out.println("默認(rèn)Comparator");? ? ? ? lists.stream().sorted().filter((s -> s.startsWith("a"))).forEach(System.out::println);System.out.println("自定義Comparator");? ? ? ? lists.stream().sorted((p1, p2) -> p2.compareTo(p1)).filter((s -> s.startsWith("a"))).forEach(System.out::println);? ? }
映射(Map)
結(jié)合Function接口,該操作能將流對象中的每個元素映射為另一種元素,實現(xiàn)元素類型的轉(zhuǎn)換。
publicstaticvoid streamMapTest() {? ? ? ? lists.stream().map(String::toUpperCase).sorted((a, b) -> b.compareTo(a)).forEach(System.out::println);? ? ? ? System.out.println("自定義映射規(guī)則");Functionfunction=(p)->{returnp +".txt";? ? ? ? };? ? ? ? lists.stream().map(String::toUpperCase).map(function).sorted((a, b)-> b.compareTo(a)).forEach(System.out::println);? ? }
在上面簡單介紹了三種常用的操作,這三種操作極大簡化了集合的處理。接下來,介紹幾種完結(jié)方法:
“變換”過程之后,需要獲取結(jié)果,即完成操作。下面我們來看相關(guān)的操作:
用來判斷某個predicate是否和流對象相匹配,最終返回Boolean類型結(jié)果,例如:
publicstaticvoid streamMatchTest() {//流對象中只要有一個元素匹配就返回trueboolean anyStartWithA = lists.stream().anyMatch((s -> s.startsWith("a")));System.out.println(anyStartWithA);//流對象中每個元素都匹配就返回trueboolean allStartWithA? ? ? ? ? ? ? ? = lists.stream().allMatch((s -> s.startsWith("a")));System.out.println(allStartWithA);? ? }
收集(Collect)
在對經(jīng)過變換之后,我們將變換的Stream的元素收集,比如將這些元素存至集合中,此時便可以使用Stream提供的collect方法,例如:
publicstaticvoidstreamCollectTest(){? ? ? ? Listlist= lists.stream().filter((p) -> p.startsWith("a")).sorted().collect(Collectors.toList());? ? ? ? System.out.println(list);? ? }
計數(shù)(Count)
類似sql的count,用來統(tǒng)計流中元素的總數(shù),例如:
publicstaticvoid streamCountTest() {? ? ? ? longcount= lists.stream().filter((s -> s.startsWith("a"))).count();System.out.println(count);? ? }
規(guī)約(Reduce)
reduce方法允許我們用自己的方式去計算元素或者將一個Stream中的元素以某種規(guī)律關(guān)聯(lián),例如:
publicstaticvoid streamReduceTest() {Optionaloptional= lists.stream().sorted().reduce((s1, s2) -> {System.out.println(s1 +"|"+ s2);returns1 +"|"+ s2;? ? ? ? });? ? }
執(zhí)行結(jié)果如下:
a1|a2
a1|a2|b1
a1|a2|b1|b2a1|a2|b1|b2|b3a1|a2|b1|b2|b3|o1
并行Stream VS 串行Stream
到目前我們已經(jīng)將常用的中間操作和完結(jié)操作介紹完了。當(dāng)然所有的的示例都是基于串行Stream。接下來介紹重點戲——并行Stream(parallel Stream)。并行Stream基于Fork-join并行分解框架實現(xiàn),將大數(shù)據(jù)集合切分為多個小數(shù)據(jù)結(jié)合交給不同的線程去處理,這樣在多核處理情況下,性能會得到很大的提高。這和MapReduce的設(shè)計理念一致:大任務(wù)化小,小任務(wù)再分配到不同的機(jī)器執(zhí)行。只不過這里的小任務(wù)是交給不同的處理器。
通過parallelStream()創(chuàng)建并行Stream。為了驗證并行Stream是否真的能提高性能,我們執(zhí)行以下測試代碼:
首先創(chuàng)建一個較大的集合:
List bigLists =newArrayList<>();for(inti =0; i <10000000; i++) {? ? ? ? ? ? UUID uuid = UUID.randomUUID();? ? ? ? ? ? bigLists.add(uuid.toString());? ? ? ? }
測試串行流下排序所用的時間:
privatestaticvoidnotParallelStreamSortedTest(List bigLists){longstartTime = System.nanoTime();longcount = bigLists.stream().sorted().count();longendTime = System.nanoTime();longmillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);? ? ? ? System.out.println(System.out.printf("串行排序: %d ms", millis));? ? }
測試并行流下排序所用的時間:
privatestaticvoidparallelStreamSortedTest(List bigLists){longstartTime = System.nanoTime();longcount = bigLists.parallelStream().sorted().count();longendTime = System.nanoTime();longmillis = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);? ? ? ? System.out.println(System.out.printf("并行排序: %d ms", millis));? ? }
結(jié)果如下:
串行排序: 13336 ms
并行排序: 6755 ms
看到這里,我們確實發(fā)現(xiàn)性能提高了約么50%,你也可能會想以后都用parallel Stream不久行了么?實則不然,如果你現(xiàn)在還是單核處理器,而數(shù)據(jù)量又不算很大的情況下,串行流仍然是這種不錯的選擇。你也會發(fā)現(xiàn)在某些情況,串行流的性能反而更好,至于具體的使用,需要你根據(jù)實際場景先測試后再決定。
上面我們談到Stream盡可能以延遲的方式運行,這里通過創(chuàng)建一個無窮大的Stream來說明:
首先通過Stream的generate方法來一個自然數(shù)序列,然后通過map變換Stream:
//遞增序列classNatureSeqimplementsSupplier{longvalue =0;@OverridepublicLongget(){? ? ? ? ? ? value++;returnvalue;? ? ? ? }? ? }publicvoidstreamCreateTest(){? ? ? ? Stream stream = Stream.generate(newNatureSeq());? ? ? ? System.out.println("元素個數(shù):"+stream.map((param) -> {returnparam;? ? ? ? }).limit(1000).count());? ? }
執(zhí)行結(jié)果為:
元素個數(shù):1000
我們發(fā)現(xiàn)開始時對這個無窮大的Stream做任何中間操作(如:filter,map等,但sorted不行)都是可以的,也就是對Stream進(jìn)行中間操作并生存一個新的Stream的過程并非立刻生效的(不然此例中的map操作會永遠(yuǎn)的運行下去,被阻塞住),當(dāng)遇到完結(jié)方法時stream才開始計算。通過limit()方法,把這個無窮的Stream轉(zhuǎn)為有窮的Stream。