Java?Lambda表達(dá)式的一個重要用法是簡化某些匿名內(nèi)部類(Anonymous Classes)的寫法掌眠。實(shí)際上Lambda表達(dá)式并不僅僅是匿名內(nèi)部類的語法糖通贞,JVM內(nèi)部是通過invokedynamic指令來實(shí)現(xiàn)Lambda表達(dá)式的苛败。具體原理放到下一篇。本篇我們首先感受一下使用Lambda表達(dá)式帶來的便利之處镰矿。
Lambda and Anonymous Classes(I)
本節(jié)將介紹如何使用Lambda表達(dá)式簡化匿名內(nèi)部類的書寫琐驴,但Lambda表達(dá)式并不能取代所有的匿名內(nèi)部類,只能用來取代函數(shù)接口(Functional Interface)的簡寫秤标。先別在乎細(xì)節(jié)绝淡,看幾個例子再說。
例子1:無參函數(shù)的簡寫
如果需要新建一個線程苍姜,一種常見的寫法是這樣:
// JDK7 匿名內(nèi)部類寫法newThread(newRunnable(){// 接口名@Overridepublicvoidrun(){// 方法名System.out.println("Thread run()");}}).start();
上述代碼給Tread類傳遞了一個匿名的Runnable對象牢酵,重載Runnable接口的run()方法來實(shí)現(xiàn)相應(yīng)邏輯。這是JDK7以及之前的常見寫法衙猪。匿名內(nèi)部類省去了為類起名字的煩惱馍乙,但還是不夠簡化,在Java 8中可以簡化為如下形式:
// JDK8 Lambda表達(dá)式寫法newThread(()->System.out.println("Thread run()")// 省略接口名和方法名).start();
上述代碼跟匿名內(nèi)部類的作用是一樣的垫释,但比匿名內(nèi)部類更進(jìn)一步丝格。這里連接口名和函數(shù)名都一同省掉了,寫起來更加神清氣爽棵譬。如果函數(shù)體有多行显蝌,可以用大括號括起來,就像這樣:
// JDK8 Lambda表達(dá)式代碼塊寫法newThread(()->{System.out.print("Hello");System.out.println(" Hoolee");}).start();
例子2:帶參函數(shù)的簡寫
如果要給一個字符串列表通過自定義比較器订咸,按照字符串長度進(jìn)行排序曼尊,Java 7的書寫形式如下:
// JDK7 匿名內(nèi)部類寫法List<String>list=Arrays.asList("I","love","you","too");Collections.sort(list,newComparator<String>(){// 接口名@Overridepublicintcompare(Strings1,Strings2){// 方法名if(s1==null)return-1;if(s2==null)return1;returns1.length()-s2.length();}});
上述代碼通過內(nèi)部類重載了Comparator接口的compare()方法,實(shí)現(xiàn)比較邏輯脏嚷。采用Lambda表達(dá)式可簡寫如下:
// JDK8 Lambda表達(dá)式寫法List<String>list=Arrays.asList("I","love","you","too");Collections.sort(list,(s1,s2)->{// 省略參數(shù)表的類型if(s1==null)return-1;if(s2==null)return1;returns1.length()-s2.length();});
上述代碼跟匿名內(nèi)部類的作用是一樣的骆撇。除了省略了接口名和方法名,代碼中把參數(shù)表的類型也省略了然眼。這得益于javac的類型推斷機(jī)制艾船,編譯器能夠根據(jù)上下文信息推斷出參數(shù)的類型葵腹,當(dāng)然也有推斷失敗的時候,這時就需要手動指明參數(shù)類型了屿岂。注意践宴,Java是強(qiáng)類型語言,每個變量和對象都必需有明確的類型爷怀。
簡寫的依據(jù)
也許你已經(jīng)想到了,能夠使用Lambda的依據(jù)是必須有相應(yīng)的函數(shù)接口(函數(shù)接口擂仍,是指內(nèi)部只有一個抽象方法的接口)亿昏。這一點(diǎn)跟Java是強(qiáng)類型語言吻合,也就是說你并不能在代碼的任何地方任性的寫Lambda表達(dá)式依疼。實(shí)際上Lambda的類型就是對應(yīng)函數(shù)接口的類型砰苍。Lambda表達(dá)式另一個依據(jù)是類型推斷機(jī)制处面,在上下文信息足夠的情況下卧斟,編譯器可以推斷出參數(shù)表的類型,而不需要顯式指名。Lambda表達(dá)更多合法的書寫形式如下:
// Lambda表達(dá)式的書寫形式Runnablerun=()->System.out.println("Hello World");// 1ActionListenerlistener=event->System.out.println("button clicked");// 2RunnablemultiLine=()->{// 3 代碼塊System.out.print("Hello");System.out.println(" Hoolee");};BinaryOperator<Long>add=(Longx,Longy)->x+y;// 4BinaryOperator<Long>addImplicit=(x,y)->x+y;// 5 類型推斷
上述代碼中再来,1展示了無參函數(shù)的簡寫;2處展示了有參函數(shù)的簡寫磷瘤,以及類型推斷機(jī)制芒篷;3是代碼塊的寫法;4和5再次展示了類型推斷機(jī)制采缚。
自定義函數(shù)接口
自定義函數(shù)接口很容易针炉,只需要編寫一個只有一個抽象方法的接口即可。
// 自定義函數(shù)接口@FunctionalInterfacepublicinterfaceConsumerInterface<T>{voidaccept(Tt);}
上面代碼中的@FunctionalInterface是可選的扳抽,但加上該標(biāo)注編譯器會幫你檢查接口是否符合函數(shù)接口規(guī)范篡帕。就像加入@Override標(biāo)注會檢查是否重載了函數(shù)一樣。
有了上述接口定義贸呢,就可以寫出類似如下的代碼:
ConsumerInterface<String> consumer = str -> System.out.println(str);
進(jìn)一步的赂苗,還可以這樣使用:
classMyStream<T>{privateList<T>list;...publicvoidmyForEach(ConsumerInterface<T>consumer){// 1for(Tt:list){consumer.accept(t);}}}MyStream<String>stream=newMyStream<String>();stream.myForEach(str->System.out.println(str));// 使用自定義函數(shù)接口書寫Lambda表達(dá)式
Lambda and Anonymous Classes(II)
讀過上一篇之后,相信對Lambda表達(dá)式的語法以及基本原理有了一定了解贮尉。對于編寫代碼,有這些知識已經(jīng)夠用朴沿。本文將進(jìn)一步區(qū)分Lambda表達(dá)式和匿名內(nèi)部類在JVM層面的區(qū)別猜谚,如果對這一部分不感興趣败砂,可以跳過。
經(jīng)過第一篇的的介紹魏铅,我們看到Lambda表達(dá)式似乎只是為了簡化匿名內(nèi)部類書寫昌犹,這看起來僅僅通過語法糖在編譯階段把所有的Lambda表達(dá)式替換成匿名內(nèi)部類就可以了。但實(shí)時并非如此览芳。在JVM層面斜姥,Lambda表達(dá)式和匿名內(nèi)部類有著明顯的差別。
匿名內(nèi)部類實(shí)現(xiàn)
匿名內(nèi)部類仍然是一個類沧竟,只是不需要程序員顯示指定類名铸敏,編譯器會自動為該類取名。因此如果有如下形式的代碼悟泵,編譯之后將會產(chǎn)生兩個class文件:
publicclassMainAnonymousClass{publicstaticvoidmain(String[]args){newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("Anonymous Class Thread run()");}}).start();;}}
編譯之后文件分布如下杈笔,兩個class文件分別是主類和匿名內(nèi)部類產(chǎn)生的:
進(jìn)一步分析主類MainAnonymousClass.class的字節(jié)碼,可發(fā)現(xiàn)其創(chuàng)建了匿名內(nèi)部類的對象:
// javap -c MainAnonymousClass.classpublicclassMainAnonymousClass{...publicstaticvoidmain(java.lang.String[]);Code:0:new#2// class java/lang/Thread3:dup4:new#3// class MainAnonymousClass$1 /*創(chuàng)建內(nèi)部類對象*/7:dup8:invokespecial#4// Method MainAnonymousClass$1."<init>":()V11:invokespecial#5// Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V14:invokevirtual#6// Method java/lang/Thread.start:()V17:return}
Lambda表達(dá)式實(shí)現(xiàn)
Lambda表達(dá)式通過invokedynamic指令實(shí)現(xiàn)糕非,書寫Lambda表達(dá)式不會產(chǎn)生新的類蒙具。如果有如下代碼,編譯之后只有一個class文件:
publicclassMainLambda{publicstaticvoidmain(String[]args){newThread(()->System.out.println("Lambda Thread run()")).start();;}}
編譯之后的結(jié)果:
通過javap反編譯命名朽肥,我們更能看出Lambda表達(dá)式內(nèi)部表示的不同:
// javap -c -p MainLambda.classpublicclassMainLambda{...publicstaticvoidmain(java.lang.String[]);Code:0:new#2// class java/lang/Thread3:dup4:invokedynamic#3,0// InvokeDynamic #0:run:()Ljava/lang/Runnable; /*使用invokedynamic指令調(diào)用*/9:invokespecial#4// Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V12:invokevirtual#5// Method java/lang/Thread.start:()V15:returnprivatestaticvoidlambda$main$0();/*Lambda表達(dá)式被封裝成主類的私有方法*/Code:0:getstatic#6// Field java/lang/System.out:Ljava/io/PrintStream;3:ldc#7// String Lambda Thread run()5:invokevirtual#8// Method java/io/PrintStream.println:(Ljava/lang/String;)V8:return}
反編譯之后我們發(fā)現(xiàn)Lambda表達(dá)式被封裝成了主類的一個私有方法禁筏,并通過invokedynamic指令進(jìn)行調(diào)用。
推論衡招,this引用的意義
既然Lambda表達(dá)式不是內(nèi)部類的簡寫篱昔,那么Lambda內(nèi)部的this引用也就跟內(nèi)部類對象沒什么關(guān)系了。在Lambda表達(dá)式中this的意義跟在表達(dá)式外部完全一樣蚁吝。因此下列代碼將輸出兩遍Hello Hoolee旱爆,而不是兩個引用地址。
publicclassHello{Runnabler1=()->{System.out.println(this);};Runnabler2=()->{System.out.println(toString());};publicstaticvoidmain(String[]args){newHello().r1.run();newHello().r2.run();}publicStringtoString(){return"Hello Hoolee";}}
Lambda and Collections
我們先從最熟悉的Java集合框架(Java Collections Framework, JCF)開始說起窘茁。
為引入Lambda表達(dá)式怀伦,Java8新增了java.util.funcion包,里面包含常用的函數(shù)接口山林,這是Lambda表達(dá)式的基礎(chǔ)房待,Java集合框架也新增部分接口,以便與Lambda表達(dá)式對接驼抹。
首先回顧一下Java集合框架的接口繼承結(jié)構(gòu):
上圖中綠色標(biāo)注的接口類桑孩,表示在Java8中加入了新的接口方法,當(dāng)然由于繼承關(guān)系框冀,他們相應(yīng)的子類也都會繼承這些新方法流椒。下表詳細(xì)列舉了這些方法。
接口名Java8新加入的方法
CollectionremoveIf() spliterator() stream() parallelStream() forEach()
ListreplaceAll() sort()
MapgetOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()
這些新加入的方法大部分要用到j(luò)ava.util.function包下的接口明也,這意味著這些方法大部分都跟Lambda表達(dá)式相關(guān)宣虾。我們將逐一學(xué)習(xí)這些方法惯裕。
Collection中的新方法
如上所示,接口Collection和List新加入了一些方法绣硝,我們以是List的子類ArrayList為例來說明蜻势。了解Java7ArrayList實(shí)現(xiàn)原理,將有助于理解下文鹉胖。
forEach()
該方法的簽名為void forEach(Consumer<? super E> action)握玛,作用是對容器中的每個元素執(zhí)行action指定的動作,其中Consumer是個函數(shù)接口甫菠,里面只有一個待實(shí)現(xiàn)方法void accept(T t)(后面我們會看到挠铲,這個方法叫什么根本不重要,你甚至不需要記憶它的名字)淑蔚。
需求:假設(shè)有一個字符串列表市殷,需要打印出其中所有長度大于3的字符串.
Java7及以前我們可以用增強(qiáng)的for循環(huán)實(shí)現(xiàn):
// 使用曾強(qiáng)for循環(huán)迭代ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));for(Stringstr:list){if(str.length()>3)System.out.println(str);}
現(xiàn)在使用forEach()方法結(jié)合匿名內(nèi)部類,可以這樣實(shí)現(xiàn):
// 使用forEach()結(jié)合匿名內(nèi)部類迭代ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.forEach(newConsumer<String>(){@Overridepublicvoidaccept(Stringstr){if(str.length()>3)System.out.println(str);}});
上述代碼調(diào)用forEach()方法刹衫,并使用匿名內(nèi)部類實(shí)現(xiàn)Comsumer接口醋寝。到目前為止我們沒看到這種設(shè)計(jì)有什么好處,但是不要忘記Lambda表達(dá)式带迟,使用Lambda表達(dá)式實(shí)現(xiàn)如下:
// 使用forEach()結(jié)合Lambda表達(dá)式迭代ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.forEach(str->{if(str.length()>3)System.out.println(str);});
上述代碼給forEach()方法傳入一個Lambda表達(dá)式音羞,我們不需要知道accept()方法,也不需要知道Consumer接口仓犬,類型推導(dǎo)幫我們做了一切嗅绰。
removeIf()
該方法簽名為boolean removeIf(Predicate<? super E> filter),作用是刪除容器中所有滿足filter指定條件的元素搀继,其中Predicate是一個函數(shù)接口窘面,里面只有一個待實(shí)現(xiàn)方法boolean test(T t),同樣的這個方法的名字根本不重要叽躯,因?yàn)橛玫臅r候不需要書寫這個名字财边。
需求:假設(shè)有一個字符串列表,需要刪除其中所有長度大于3的字符串点骑。
我們知道如果需要在迭代過程沖對容器進(jìn)行刪除操作必須使用迭代器酣难,否則會拋出ConcurrentModificationException,所以上述任務(wù)傳統(tǒng)的寫法是:
// 使用迭代器刪除列表元素ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));Iterator<String>it=list.iterator();while(it.hasNext()){if(it.next().length()>3)// 刪除長度大于3的元素it.remove();}
現(xiàn)在使用removeIf()方法結(jié)合匿名內(nèi)部類黑滴,我們可是這樣實(shí)現(xiàn):
// 使用removeIf()結(jié)合匿名名內(nèi)部類實(shí)現(xiàn)ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.removeIf(newPredicate<String>(){// 刪除長度大于3的元素@Overridepublicbooleantest(Stringstr){returnstr.length()>3;}});
上述代碼使用removeIf()方法憨募,并使用匿名內(nèi)部類實(shí)現(xiàn)Precicate接口。相信你已經(jīng)想到用Lambda表達(dá)式該怎么寫了:
// 使用removeIf()結(jié)合Lambda表達(dá)式實(shí)現(xiàn)ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.removeIf(str->str.length()>3);// 刪除長度大于3的元素
使用Lambda表達(dá)式不需要記憶Predicate接口名袁辈,也不需要記憶test()方法名菜谣,只需要知道此處需要一個返回布爾類型的Lambda表達(dá)式就行了。
replaceAll()
該方法簽名為void replaceAll(UnaryOperator<E> operator),作用是對每個元素執(zhí)行operator指定的操作葛菇,并用操作結(jié)果來替換原來的元素甘磨。其中UnaryOperator是一個函數(shù)接口,里面只有一個待實(shí)現(xiàn)函數(shù)T apply(T t)眯停。
需求:假設(shè)有一個字符串列表,將其中所有長度大于3的元素轉(zhuǎn)換成大寫卿泽,其余元素不變莺债。
Java7及之前似乎沒有優(yōu)雅的辦法:
// 使用下標(biāo)實(shí)現(xiàn)元素替換ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));for(inti=0;i<list.size();i++){Stringstr=list.get(i);if(str.length()>3)list.set(i,str.toUpperCase());}
使用replaceAll()方法結(jié)合匿名內(nèi)部類可以實(shí)現(xiàn)如下:
// 使用匿名內(nèi)部類實(shí)現(xiàn)ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.replaceAll(newUnaryOperator<String>(){@OverridepublicStringapply(Stringstr){if(str.length()>3)returnstr.toUpperCase();returnstr;}});
上述代碼調(diào)用replaceAll()方法,并使用匿名內(nèi)部類實(shí)現(xiàn)UnaryOperator接口签夭。我們知道可以用更為簡潔的Lambda表達(dá)式實(shí)現(xiàn):
// 使用Lambda表達(dá)式實(shí)現(xiàn)ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.replaceAll(str->{if(str.length()>3)returnstr.toUpperCase();returnstr;});
sort()
該方法定義在List接口中齐邦,方法簽名為void sort(Comparator<? super E> c),該方法根據(jù)c指定的比較規(guī)則對容器元素進(jìn)行排序第租。Comparator接口我們并不陌生措拇,其中有一個方法int compare(T o1, T o2)需要實(shí)現(xiàn),顯然該接口是個函數(shù)接口慎宾。
需求:假設(shè)有一個字符串列表丐吓,按照字符串長度增序?qū)υ嘏判颉?/i>
由于Java7以及之前sort()方法在Collections工具類中,所以代碼要這樣寫:
// Collections.sort()方法ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));Collections.sort(list,newComparator<String>(){@Overridepublicintcompare(Stringstr1,Stringstr2){returnstr1.length()-str2.length();}});
現(xiàn)在可以直接使用List.sort()方法趟据,結(jié)合Lambda表達(dá)式券犁,可以這樣寫:
// List.sort()方法結(jié)合Lambda表達(dá)式ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.sort((str1,str2)->str1.length()-str2.length());
spliterator()
方法簽名為Spliterator<E> spliterator(),該方法返回容器的可拆分迭代器汹碱。從名字來看該方法跟iterator()方法有點(diǎn)像粘衬,我們知道Iterator是用來迭代容器的,Spliterator也有類似作用咳促,但二者有如下不同:
Spliterator既可以像Iterator那樣逐個迭代稚新,也可以批量迭代。批量迭代可以降低迭代的開銷跪腹。
Spliterator是可拆分的褂删,一個Spliterator可以通過調(diào)用Spliterator<T> trySplit()方法來嘗試分成兩個。一個是this尺迂,另一個是新返回的那個笤妙,這兩個迭代器代表的元素沒有重疊。
可通過(多次)調(diào)用Spliterator.trySplit()方法來分解負(fù)載噪裕,以便多線程處理蹲盘。
stream()和parallelStream()
stream()和parallelStream()分別返回該容器的Stream視圖表示,不同之處在于parallelStream()返回并行的Stream膳音。Stream是Java函數(shù)式編程的核心類召衔,我們會在后面章節(jié)中學(xué)習(xí)。
Map中的新方法
相比Collection祭陷,Map中加入了更多的方法苍凛,我們以HashMap為例來逐一探秘趣席。了解Java7HashMap實(shí)現(xiàn)原理,將有助于理解下文醇蝴。
forEach()
該方法簽名為void forEach(BiConsumer<? super K,? super V> action)宣肚,作用是對Map中的每個映射執(zhí)行action指定的操作,其中BiConsumer是一個函數(shù)接口悠栓,里面有一個待實(shí)現(xiàn)方法void accept(T t, U u)霉涨。BinConsumer接口名字和accept()方法名字都不重要,請不要記憶他們惭适。
需求:假設(shè)有一個數(shù)字到對應(yīng)英文單詞的Map笙瑟,請輸出Map中的所有映射關(guān)系.
Java7以及之前經(jīng)典的代碼如下:
// Java7以及之前迭代MapHashMap<Integer,String>map=newHashMap<>();map.put(1,"one");map.put(2,"two");map.put(3,"three");for(Map.Entry<Integer,String>entry:map.entrySet()){System.out.println(entry.getKey()+"="+entry.getValue());}
使用Map.forEach()方法,結(jié)合匿名內(nèi)部類癞志,代碼如下:
// 使用forEach()結(jié)合匿名內(nèi)部類迭代MapHashMap<Integer,String>map=newHashMap<>();map.put(1,"one");map.put(2,"two");map.put(3,"three");map.forEach(newBiConsumer<Integer,String>(){@Overridepublicvoidaccept(Integerk,Stringv){System.out.println(k+"="+v);}});
上述代碼調(diào)用forEach()方法往枷,并使用匿名內(nèi)部類實(shí)現(xiàn)BiConsumer接口。當(dāng)然凄杯,實(shí)際場景中沒人使用匿名內(nèi)部類寫法错洁,因?yàn)橛蠰ambda表達(dá)式:
// 使用forEach()結(jié)合Lambda表達(dá)式迭代MapHashMap<Integer,String>map=newHashMap<>();map.put(1,"one");map.put(2,"two");map.put(3,"three");map.forEach((k,v)->System.out.println(k+"="+v));}
getOrDefault()
該方法跟Lambda表達(dá)式?jīng)]關(guān)系,但是很有用盾舌。方法簽名為V getOrDefault(Object key, V defaultValue)墓臭,作用是按照給定的key查詢Map中對應(yīng)的value,如果沒有找到則返回defaultValue妖谴。使用該方法程序員可以省去查詢指定鍵值是否存在的麻煩.
需求窿锉;假設(shè)有一個數(shù)字到對應(yīng)英文單詞的Map,輸出4對應(yīng)的英文單詞膝舅,如果不存在則輸出NoValue
// 查詢Map中指定的值嗡载,不存在時使用默認(rèn)值HashMap<Integer,String>map=newHashMap<>();map.put(1,"one");map.put(2,"two");map.put(3,"three");// Java7以及之前做法if(map.containsKey(4)){// 1System.out.println(map.get(4));}else{System.out.println("NoValue");}// Java8使用Map.getOrDefault()System.out.println(map.getOrDefault(4,"NoValue"));// 2
putIfAbsent()
該方法跟Lambda表達(dá)式?jīng)]關(guān)系,但是很有用仍稀。方法簽名為V putIfAbsent(K key, V value)洼滚,作用是只有在不存在key值的映射或映射值為null時,才將value指定的值放入到Map中技潘,否則不對Map做更改.該方法將條件判斷和賦值合二為一遥巴,使用起來更加方便.
remove()
我們都知道Map中有一個remove(Object key)方法,來根據(jù)指定key值刪除Map中的映射關(guān)系享幽;Java8新增了remove(Object key, Object value)方法铲掐,只有在當(dāng)前Map中key正好映射到value時才刪除該映射,否則什么也不做.
replace()
在Java7及以前值桩,要想替換Map中的映射關(guān)系可通過put(K key, V value)方法實(shí)現(xiàn)摆霉,該方法總是會用新值替換原來的值.為了更精確的控制替換行為,Java8在Map中加入了兩個replace()方法,分別如下:
replace(K key, V value)携栋,只有在當(dāng)前Map中key的映射存在時才用value去替換原來的值搭盾,否則什么也不做.
replace(K key, V oldValue, V newValue),只有在當(dāng)前Map中key的映射存在且等于oldValue時才用newValue去替換原來的值婉支,否則什么也不做.
replaceAll()
該方法簽名為replaceAll(BiFunction<? super K,? super V,? extends V> function)鸯隅,作用是對Map中的每個映射執(zhí)行function指定的操作,并用function的執(zhí)行結(jié)果替換原來的value向挖,其中BiFunction是一個函數(shù)接口滋迈,里面有一個待實(shí)現(xiàn)方法R apply(T t, U u).不要被如此多的函數(shù)接口嚇到,因?yàn)槭褂玫臅r候根本不需要知道他們的名字.
需求:假設(shè)有一個數(shù)字到對應(yīng)英文單詞的Map户誓,請將原來映射關(guān)系中的單詞都轉(zhuǎn)換成大寫.
Java7以及之前經(jīng)典的代碼如下:
// Java7以及之前替換所有Map中所有映射關(guān)系HashMap<Integer,String>map=newHashMap<>();map.put(1,"one");map.put(2,"two");map.put(3,"three");for(Map.Entry<Integer,String>entry:map.entrySet()){entry.setValue(entry.getValue().toUpperCase());}
使用replaceAll()方法結(jié)合匿名內(nèi)部類,實(shí)現(xiàn)如下:
// 使用replaceAll()結(jié)合匿名內(nèi)部類實(shí)現(xiàn)HashMap<Integer,String>map=newHashMap<>();map.put(1,"one");map.put(2,"two");map.put(3,"three");map.replaceAll(newBiFunction<Integer,String,String>(){@OverridepublicStringapply(Integerk,Stringv){returnv.toUpperCase();}});
上述代碼調(diào)用replaceAll()方法幕侠,并使用匿名內(nèi)部類實(shí)現(xiàn)BiFunction接口帝美。更進(jìn)一步的,使用Lambda表達(dá)式實(shí)現(xiàn)如下:
// 使用replaceAll()結(jié)合Lambda表達(dá)式實(shí)現(xiàn)HashMap<Integer,String>map=newHashMap<>();map.put(1,"one");map.put(2,"two");map.put(3,"three");map.replaceAll((k,v)->v.toUpperCase());
簡潔到讓人難以置信.
merge()
該方法簽名為merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)晤硕,作用是:
如果Map中key對應(yīng)的映射不存在或者為null悼潭,則將value(不能是null)關(guān)聯(lián)到key上;
否則執(zhí)行remappingFunction舞箍,如果執(zhí)行結(jié)果非null則用該結(jié)果跟key關(guān)聯(lián)舰褪,否則在Map中刪除key的映射.
參數(shù)中BiFunction函數(shù)接口前面已經(jīng)介紹過,里面有一個待實(shí)現(xiàn)方法R apply(T t, U u).
merge()方法雖然語義有些復(fù)雜疏橄,但該方法的用方式很明確占拍,一個比較常見的場景是將新的錯誤信息拼接到原來的信息上,比如:
map.merge(key,newMsg,(v1,v2)->v1+v2);
compute()
該方法簽名為compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)捎迫,作用是把remappingFunction的計(jì)算結(jié)果關(guān)聯(lián)到key上晃酒,如果計(jì)算結(jié)果為null,則在Map中刪除key的映射.
要實(shí)現(xiàn)上述merge()方法中錯誤信息拼接的例子窄绒,使用compute()代碼如下:
map.compute(key,(k,v)->v==null?newMsg:v.concat(newMsg));
computeIfAbsent()
該方法簽名為V computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)贝次,作用是:只有在當(dāng)前Map中不存在key值的映射或映射值為null時,才調(diào)用mappingFunction彰导,并在mappingFunction執(zhí)行結(jié)果非null時蛔翅,將結(jié)果跟key關(guān)聯(lián).
Function是一個函數(shù)接口,里面有一個待實(shí)現(xiàn)方法R apply(T t).
computeIfAbsent()常用來對Map的某個key值建立初始化映射.比如我們要實(shí)現(xiàn)一個多值映射位谋,Map的定義可能是Map<K,Set<V>>山析,要向Map中放入新值,可通過如下代碼實(shí)現(xiàn):
Map<Integer,Set<String>>map=newHashMap<>();// Java7及以前的實(shí)現(xiàn)方式if(map.containsKey(1)){map.get(1).add("one");}else{Set<String>valueSet=newHashSet<String>();valueSet.add("one");map.put(1,valueSet);}// Java8的實(shí)現(xiàn)方式map.computeIfAbsent(1,v->newHashSet<String>()).add("yi");
使用computeIfAbsent()將條件判斷和添加操作合二為一倔幼,使代碼更加簡潔.
computeIfPresent()
該方法簽名為V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)盖腿,作用跟computeIfAbsent()相反埃儿,即衩藤,只有在當(dāng)前Map中存在key值的映射且非null時,才調(diào)用remappingFunction,如果remappingFunction執(zhí)行結(jié)果為null蟋滴,則刪除key的映射,否則使用該結(jié)果替換key原來的映射.
這個函數(shù)的功能跟如下代碼是等效的:
// Java7及以前跟computeIfPresent()等效的代碼if(map.get(key)!=null){VoldValue=map.get(key);VnewValue=remappingFunction.apply(key,oldValue);if(newValue!=null)map.put(key,newValue);elsemap.remove(key);returnnewValue;}returnnull;
Java8為容器新增一些有用的方法豁生,這些方法有些是為完善原有功能譬巫,有些是為引入函數(shù)式編程,學(xué)習(xí)和使用這些方法有助于我們寫出更加簡潔有效的代碼.
函數(shù)接口雖然很多等龙,但絕大多數(shù)時候我們根本不需要知道它們的名字处渣,書寫Lambda表達(dá)式時類型推斷幫我們做了一切.
Streams API(I)
你可能沒意識到Java對函數(shù)式編程的重視程度,看看Java 8加入函數(shù)式編程擴(kuò)充多少功能就清楚了蛛砰。Java 8之所以費(fèi)這么大功夫引入函數(shù)式編程罐栈,原因有二:
代碼簡潔函數(shù)式編程寫出的代碼簡潔且意圖明確,使用stream接口讓你從此告別for循環(huán)泥畅。
多核友好荠诬,Java函數(shù)式編程使得編寫并行程序從未如此簡單,你需要的全部就是調(diào)用一下parallel()方法位仁。
這一節(jié)我們學(xué)習(xí)stream柑贞,也就是Java函數(shù)式編程的主角。對于Java 7來說stream完全是個陌生東西聂抢,stream并不是某種數(shù)據(jù)結(jié)構(gòu)钧嘶,它只是數(shù)據(jù)源的一種視圖。這里的數(shù)據(jù)源可以是一個數(shù)組琳疏,Java容器或I/O channel等有决。正因如此要得到一個stream通常不會手動創(chuàng)建,而是調(diào)用對應(yīng)的工具方法轿亮,比如:
調(diào)用Collection.stream()或者Collection.parallelStream()方法
調(diào)用Arrays.stream(T[] array)方法
常見的stream接口繼承關(guān)系如圖:
圖中4種stream接口繼承自BaseStream疮薇,其中IntStream, LongStream, DoubleStream對應(yīng)三種基本類型(int, long, double,注意不是包裝類型)我注,Stream對應(yīng)所有剩余類型的stream視圖按咒。為不同數(shù)據(jù)類型設(shè)置不同stream接口,可以1.提高性能但骨,2.增加特定接口函數(shù)励七。
你可能會奇怪為什么不把IntStream等設(shè)計(jì)成Stream的子接口?畢竟這接口中的方法名大部分是一樣的奔缠。答案是這些方法的名字雖然相同掠抬,但是返回類型不同,如果設(shè)計(jì)成父子接口關(guān)系校哎,這些方法將不能共存两波,因?yàn)镴ava不允許只有返回類型不同的方法重載瞳步。
雖然大部分情況下stream是容器調(diào)用Collection.stream()方法得到的,但stream和collections有以下不同:
無存儲腰奋。stream不是一種數(shù)據(jù)結(jié)構(gòu)单起,它只是某種數(shù)據(jù)源的一個視圖,數(shù)據(jù)源可以是一個數(shù)組劣坊,Java容器或I/O channel等嘀倒。
為函數(shù)式編程而生。對stream的任何修改都不會修改背后的數(shù)據(jù)源局冰,比如對stream執(zhí)行過濾操作并不會刪除被過濾的元素测蘑,而是會產(chǎn)生一個不包含被過濾元素的新stream。
惰式執(zhí)行康二。stream上的操作并不會立即執(zhí)行碳胳,只有等到用戶真正需要結(jié)果的時候才會執(zhí)行。
可消費(fèi)性沫勿。stream只能被“消費(fèi)”一次固逗,一旦遍歷過就會失效,就像容器的迭代器那樣藕帜,想要再次遍歷必須重新生成。
對stream的操作分為為兩類惜傲,中間操作(intermediate operations)和結(jié)束操作(terminal operations)洽故,二者特點(diǎn)是:
中間操作總是會惰式執(zhí)行,調(diào)用中間操作只會生成一個標(biāo)記了該操作的新stream盗誊,僅此而已时甚。
結(jié)束操作會觸發(fā)實(shí)際計(jì)算,計(jì)算發(fā)生時會把所有中間操作積攢的操作以pipeline的方式執(zhí)行哈踱,這樣可以減少迭代次數(shù)荒适。計(jì)算完成之后stream就會失效。
如果你熟悉Apache Spark RDD开镣,對stream的這個特點(diǎn)應(yīng)該不陌生刀诬。
下表匯總了Stream接口的部分常見方法:
操作類型接口方法
中間操作concat() distinct() filter() flatMap() limit() map() peek()
skip() sorted() parallel() sequential() unordered()
結(jié)束操作allMatch() anyMatch() collect() count() findAny() findFirst()
forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()
區(qū)分中間操作和結(jié)束操作最簡單的方法,就是看方法的返回值邪财,返回值為stream的大都是中間操作陕壹,否則是結(jié)束操作。
stream方法使用
stream跟函數(shù)接口關(guān)系非常緊密树埠,沒有函數(shù)接口stream就無法工作糠馆。回顧一下:函數(shù)接口是指內(nèi)部只有一個抽象方法的接口怎憋。通常函數(shù)接口出現(xiàn)的地方都可以使用Lambda表達(dá)式又碌,所以不必記憶函數(shù)接口的名字九昧。
forEach()
我們對forEach()方法并不陌生,在Collection中我們已經(jīng)見過毕匀。方法簽名為void forEach(Consumer<? super E> action)铸鹰,作用是對容器中的每個元素執(zhí)行action指定的動作,也就是對元素進(jìn)行遍歷期揪。
// 使用Stream.forEach()迭代Stream<String>stream=Stream.of("I","love","you","too");stream.forEach(str->System.out.println(str));
由于forEach()是結(jié)束方法掉奄,上述代碼會立即執(zhí)行,輸出所有字符串凤薛。
filter()
函數(shù)原型為Stream<T> filter(Predicate<? super T> predicate)姓建,作用是返回一個只包含滿足predicate條件元素的Stream。
// 保留長度等于3的字符串Stream<String>stream=Stream.of("I","love","you","too");stream.filter(str->str.length()==3).forEach(str->System.out.println(str));
上述代碼將輸出為長度等于3的字符串you和too缤苫。注意速兔,由于filter()是個中間操作,如果只調(diào)用filter()不會有實(shí)際計(jì)算活玲,因此也不會輸出任何信息涣狗。
distinct()
函數(shù)原型為Stream<T> distinct(),作用是返回一個去除重復(fù)元素之后的Stream舒憾。
Stream<String>stream=Stream.of("I","love","you","too","too");stream.distinct().forEach(str->System.out.println(str));
上述代碼會輸出去掉一個too之后的其余字符串镀钓。
sorted()
排序函數(shù)有兩個,一個是用自然順序排序镀迂,一個是使用自定義比較器排序丁溅,函數(shù)原型分別為Stream<T> sorted()和Stream<T> sorted(Comparator<? super T> comparator)。
Stream<String>stream=Stream.of("I","love","you","too");stream.sorted((str1,str2)->str1.length()-str2.length()).forEach(str->System.out.println(str));
上述代碼將輸出按照長度升序排序后的字符串探遵,結(jié)果完全在預(yù)料之中窟赏。
map()
函數(shù)原型為<R> Stream<R> map(Function<? super T,? extends R> mapper),作用是返回一個對當(dāng)前所有元素執(zhí)行執(zhí)行mapper之后的結(jié)果組成的Stream箱季。直觀的說涯穷,就是對每個元素按照某種操作進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換前后Stream中元素的個數(shù)不會改變藏雏,但元素的類型取決于轉(zhuǎn)換之后的類型拷况。
Stream<String>stream =Stream.of("I","love","you","too");stream.map(str->str.toUpperCase()).forEach(str->System.out.println(str));
上述代碼將輸出原字符串的大寫形式。
flatMap()
函數(shù)原型為<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)掘殴,作用是對每個元素執(zhí)行mapper指定的操作蝠嘉,并用所有mapper返回的Stream中的元素組成一個新的Stream作為最終返回結(jié)果。說起來太拗口杯巨,通俗的講flatMap()的作用就相當(dāng)于把原stream中的所有元素都”攤平”之后組成的Stream蚤告,轉(zhuǎn)換前后元素的個數(shù)和類型都可能會改變。
Stream<List<Integer>>stream=Stream.of(Arrays.asList(1,2),Arrays.asList(3,4,5));stream.flatMap(list->list.stream()).forEach(i->System.out.println(i));
上述代碼中服爷,原來的stream中有兩個元素杜恰,分別是兩個List<Integer>获诈,執(zhí)行flatMap()之后,將每個List都“攤平”成了一個個的數(shù)字心褐,所以會新產(chǎn)生一個由5個數(shù)字組成的Stream舔涎。所以最終將輸出1~5這5個數(shù)字。
截止到目前我們感覺良好逗爹,已介紹Stream接口函數(shù)理解起來并不費(fèi)勁兒亡嫌。如果你就此以為函數(shù)式編程不過如此,恐怕是高興地太早了掘而。下一節(jié)對Stream規(guī)約操作的介紹將刷新你現(xiàn)在的認(rèn)識挟冠。
Streams API(II)
上一節(jié)介紹了部分Stream常見接口方法,理解起來并不困難袍睡,但Stream的用法不止于此知染,本節(jié)我們將仍然以Stream為例,介紹流的規(guī)約操作斑胜。
規(guī)約操作(reduction operation)又被稱作折疊操作(fold)控淡,是通過某個連接動作將所有元素匯總成一個匯總結(jié)果的過程。元素求和止潘、求最大值或最小值掺炭、求出元素總個數(shù)、將所有元素轉(zhuǎn)換成一個列表或集合凭戴,都屬于規(guī)約操作竹伸。Stream類庫有兩個通用的規(guī)約操作reduce()和collect(),也有一些為簡化書寫而設(shè)計(jì)的專用規(guī)約操作簇宽,比如sum()、max()吧享、min()魏割、count()等。
最大或最小值這類規(guī)約操作很好理解(至少方法語義上是這樣)钢颂,我們著重介紹reduce()和collect()钞它,這是比較有魔法的地方。
多面手reduce()
reduce操作可以實(shí)現(xiàn)從一組元素中生成一個值殊鞭,sum()遭垛、max()、min()锯仪、count()等都是reduce操作,將他們單獨(dú)設(shè)為函數(shù)只是因?yàn)槌S弥貉巍educe()的方法定義有三種重寫形式:
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
雖然函數(shù)定義越來越長庶喜,但語義不曾改變小腊,多的參數(shù)只是為了指明初始值(參數(shù)identity),或者是指定并行執(zhí)行時多個部分結(jié)果的合并方式(參數(shù)combiner)久窟。reduce()最常用的場景就是從一堆值中生成一個值秩冈。用這么復(fù)雜的函數(shù)去求一個最大或最小值,你是不是覺得設(shè)計(jì)者有病斥扛。其實(shí)不然入问,因?yàn)椤按蟆焙汀靶 被蛘摺扒蠛汀庇袝r會有不同的語義。
需求:從一組單詞中找出最長的單詞稀颁。這里“大”的含義就是“長”芬失。
// 找出最長的單詞Stream<String>stream=Stream.of("I","love","you","too");Optional<String>longest=stream.reduce((s1,s2)->s1.length()>=s2.length()?s1:s2);//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());System.out.println(longest.get());
上述代碼會選出最長的單詞love,其中Optional是(一個)值的容器峻村,使用它可以避免null值的麻煩麸折。當(dāng)然可以使用Stream.max(Comparator<? super T> comparator)方法來達(dá)到同等效果,但reduce()自有其存在的理由粘昨。
需求:求出一組單詞的長度之和垢啼。這是個“求和”操作,操作對象輸入類型是String张肾,而結(jié)果類型是Integer芭析。
// 求單詞長度之和Stream<String>stream=Stream.of("I","love","you","too");IntegerlengthSum=stream.reduce(0, // 初始值 // (1)(sum,str)->sum+str.length(),// 累加器 // (2)(a,b)->a+b); // 部分和拼接器,并行執(zhí)行時才會用到 // (3)// int lengthSum = stream.mapToInt(str -> str.length()).sum();System.out.println(lengthSum);
上述代碼標(biāo)號(2)處將i. 字符串映射成長度吞瞪,ii. 并和當(dāng)前累加和相加馁启。這顯然是兩步操作,使用reduce()函數(shù)將這兩步合二為一芍秆,更有助于提升性能惯疙。如果想要使用map()和sum()組合來達(dá)到上述目的,也是可以的妖啥。
reduce()擅長的是生成一個值霉颠,如果想要從Stream生成一個集合或者Map等復(fù)雜的對象該怎么辦呢?終極武器collect()橫空出世荆虱!
終極武器collect()
不夸張的講蒿偎,如果你發(fā)現(xiàn)某個功能在Stream接口中沒找到,十有八九可以通過collect()方法實(shí)現(xiàn)怀读。collect()是Stream接口方法中最靈活的一個诉位,學(xué)會它才算真正入門Java函數(shù)式編程。先看幾個熱身的小例子:
// 將Stream轉(zhuǎn)換成容器或MapStream<String>stream=Stream.of("I","love","you","too");List<String>list=stream.collect(Collectors.toList());// (1)// Set<String> set = stream.collect(Collectors.toSet()); // (2)// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)
上述代碼分別列舉了如何將Stream轉(zhuǎn)換成List菜枷、Set和Map苍糠。雖然代碼語義很明確,可是我們?nèi)匀粫袔讉€疑問:
Function.identity()是干什么的啤誊?
String::length是什么意思椿息?
Collectors是個什么東西歹袁?
接口的靜態(tài)方法和默認(rèn)方法
Function是一個接口,那么Function.identity()是什么意思呢寝优?這要從兩方面解釋:
Java 8允許在接口中加入具體方法条舔。接口中的具體方法有兩種,default方法和static方法乏矾,identity()就是Function接口的一個靜態(tài)方法孟抗。
Function.identity()返回一個輸出跟輸入一樣的Lambda表達(dá)式對象,等價于形如t -> t形式的Lambda表達(dá)式钻心。
上面的解釋是不是讓你疑問更多凄硼?不要問我為什么接口中可以有具體方法,也不要告訴我你覺得t -> t比identity()方法更直觀捷沸。我會告訴你接口中的default方法是一個無奈之舉摊沉,在Java 7及之前要想在定義好的接口中加入新的抽象方法是很困難甚至不可能的,因?yàn)樗袑?shí)現(xiàn)了該接口的類都要重新實(shí)現(xiàn)痒给。試想在Collection接口中加入一個stream()抽象方法會怎樣说墨?default方法就是用來解決這個尷尬問題的,直接在接口中實(shí)現(xiàn)新加入的方法苍柏。既然已經(jīng)引入了default方法尼斧,為何不再加入static方法來避免專門的工具類呢!
方法引用
諸如String::length的語法形式叫做方法引用(method references)试吁,這種語法用來替代某些特定形式Lambda表達(dá)式棺棵。如果Lambda表達(dá)式的全部內(nèi)容就是調(diào)用一個已有的方法,那么可以用方法引用來替代Lambda表達(dá)式熄捍。方法引用可以細(xì)分為四類:
方法引用類別舉例
引用靜態(tài)方法Integer::sum
引用某個對象的方法list::add
引用某個類的方法String::length
引用構(gòu)造方法HashMap::new
我們會在后面的例子中使用方法引用烛恤。
收集器
相信前面繁瑣的內(nèi)容已徹底打消了你學(xué)習(xí)Java函數(shù)式編程的熱情,不過很遺憾余耽,下面的內(nèi)容更繁瑣缚柏。但這不能怪Stream類庫,因?yàn)橐獙?shí)現(xiàn)的功能本身很復(fù)雜宾添。
收集器(Collector)是為Stream.collect()方法量身打造的工具接口(類)」衤悖考慮一下將一個Stream轉(zhuǎn)換成一個容器(或者Map)需要做哪些工作缕陕?我們至少需要兩樣?xùn)|西:
目標(biāo)容器是什么?是ArrayList還是HashSet疙挺,或者是個TreeMap扛邑。
新元素如何添加到容器中?是List.add()還是Map.put()铐然。
如果并行的進(jìn)行規(guī)約蔬崩,還需要告訴collect()?3. 多個部分結(jié)果如何合并成一個恶座。
結(jié)合以上分析,collect()方法定義為<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)沥阳,三個參數(shù)依次對應(yīng)上述三條分析跨琳。不過每次調(diào)用collect()都要傳入這三個參數(shù)太麻煩,收集器Collector就是對這三個參數(shù)的簡單封裝,所以collect()的另一定義為<R,A> R collect(Collector<? super T,A,R> collector)桐罕。Collectors工具類可通過靜態(tài)方法生成各種常用的Collector脉让。舉例來說,如果要將Stream規(guī)約成List可以通過如下兩種方式實(shí)現(xiàn):
// 將Stream規(guī)約成ListStream<String>stream=Stream.of("I","love","you","too");List<String>list=stream.collect(ArrayList::new,ArrayList::add,ArrayList::addAll);// 方式1//List<String> list = stream.collect(Collectors.toList());// 方式2System.out.println(list);
通常情況下我們不需要手動指定collect()的三個參數(shù)功炮,而是調(diào)用collect(Collector<? super T,A,R> collector)方法溅潜,并且參數(shù)中的Collector對象大都是直接通過Collectors工具類獲得。實(shí)際上傳入的收集器的行為決定了collect()的行為薪伏。
使用collect()生成Collection
前面已經(jīng)提到通過collect()方法將Stream轉(zhuǎn)換成容器的方法滚澜,這里再匯總一下。將Stream轉(zhuǎn)換成List或Set是比較常見的操作嫁怀,所以Collectors工具已經(jīng)為我們提供了對應(yīng)的收集器设捐,通過如下代碼即可完成:
// 將Stream轉(zhuǎn)換成List或SetStream<String>stream=Stream.of("I","love","you","too");List<String>list=stream.collect(Collectors.toList());// (1)Set<String>set=stream.collect(Collectors.toSet());// (2)
上述代碼能夠滿足大部分需求,但由于返回結(jié)果是接口類型眶掌,我們并不知道類庫實(shí)際選擇的容器類型是什么挡育,有時候我們可能會想要人為指定容器的實(shí)際類型,這個需求可通過Collectors.toCollection(Supplier<C> collectionFactory)方法完成朴爬。
// 使用toCollection()指定規(guī)約容器的類型ArrayList<String>arrayList=stream.collect(Collectors.toCollection(ArrayList::new));// (3)HashSet<String>hashSet=stream.collect(Collectors.toCollection(HashSet::new));// (4)
上述代碼(3)處指定規(guī)約結(jié)果是ArrayList即寒,而(4)處指定規(guī)約結(jié)果為HashSet。一切如你所愿召噩。
使用collect()生成Map
前面已經(jīng)說過Stream背后依賴于某種數(shù)據(jù)源母赵,數(shù)據(jù)源可以是數(shù)組、容器等具滴,但不能是Map凹嘲。反過來從Stream生成Map是可以的,但我們要想清楚Map的key和value分別代表什么构韵,根本原因是我們要想清楚要干什么周蹭。通常在三種情況下collect()的結(jié)果會是Map:
使用Collectors.toMap()生成的收集器,用戶需要指定如何生成Map的key和value疲恢。
使用Collectors.partitioningBy()生成的收集器凶朗,對元素進(jìn)行二分區(qū)操作時用到。
使用Collectors.groupingBy()生成的收集器显拳,對元素做group操作時用到棚愤。
情況1:使用toMap()生成的收集器,這種情況是最直接的,前面例子中已提到宛畦,這是和Collectors.toCollection()并列的方法瘸洛。如下代碼展示將學(xué)生列表轉(zhuǎn)換成由<學(xué)生,GPA>組成的Map次和。非常直觀反肋,無需多言。
// 使用toMap()統(tǒng)計(jì)學(xué)生GPAMap<Student,Double>studentToGPA=students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成keystudent->computeGPA(student)));// 如何生成value
情況2:使用partitioningBy()生成的收集器斯够,這種情況適用于將Stream中的元素依據(jù)某個二值邏輯(滿足條件囚玫,或不滿足)分成互補(bǔ)相交的兩部分,比如男女性別读规、成績及格與否等抓督。下列代碼展示將學(xué)生分成成績及格或不及格的兩部分。
// Partition students into passing and failingMap<Boolean,List<Student>>passingFailing=students.stream().collect(Collectors.partitioningBy(s->s.getGrade()>=PASS_THRESHOLD));
情況3:使用groupingBy()生成的收集器束亏,這是比較靈活的一種情況铃在。跟SQL中的group by語句類似,這里的groupingBy()也是按照某個屬性對數(shù)據(jù)進(jìn)行分組碍遍,屬性相同的元素會被對應(yīng)到Map的同一個key上定铜。下列代碼展示將員工按照部門進(jìn)行分組:
// Group employees by departmentMap<Department,List<Employee>>byDept=employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));
以上只是分組的最基本用法,有些時候僅僅分組是不夠的怕敬。在SQL中使用group by是為了協(xié)助其他查詢揣炕,比如1. 先將員工按照部門分組,2. 然后統(tǒng)計(jì)每個部門員工的人數(shù)东跪。Java類庫設(shè)計(jì)者也考慮到了這種情況畸陡,增強(qiáng)版的groupingBy()能夠滿足這種需求。增強(qiáng)版的groupingBy()允許我們對元素分組之后再執(zhí)行某種運(yùn)算虽填,比如求和丁恭、計(jì)數(shù)、平均值斋日、類型轉(zhuǎn)換等牲览。這種先將元素分組的收集器叫做上游收集器,之后執(zhí)行其他運(yùn)算的收集器叫做下游收集器(downstream Collector)恶守。
// 使用下游收集器統(tǒng)計(jì)每個部門的人數(shù)Map<Department,Integer>totalByDept=employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.counting()));// 下游收集器
上面代碼的邏輯是不是越看越像SQL第献?高度非結(jié)構(gòu)化。還有更狠的兔港,下游收集器還可以包含更下游的收集器庸毫,這絕不是為了炫技而增加的把戲,而是實(shí)際場景需要押框〔沓瘢考慮將員工按照部門分組的場景理逊,如果我們想得到每個員工的名字(字符串)橡伞,而不是一個個Employee對象盒揉,可通過如下方式做到:
// 按照部門對員工分布組,并只保留員工的名字Map<Department,List<String>>byDept=employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.mapping(Employee::getName,// 下游收集器Collectors.toList())));// 更下游的收集器
如果看到這里你還沒有對Java函數(shù)式編程失去信心兑徘,恭喜你刚盈,你已經(jīng)順利成為Java函數(shù)式編程大師了。
使用collect()做字符串join
這個肯定是大家喜聞樂見的功能挂脑,字符串拼接時使用Collectors.joining()生成的收集器藕漱,從此告別for循環(huán)。Collectors.joining()方法有三種重寫形式崭闲,分別對應(yīng)三種不同的拼接方式肋联。無需多言,代碼過目難忘刁俭。
// 使用Collectors.joining()拼接字符串Stream<String>stream=Stream.of("I","love","you");//String joined = stream.collect(Collectors.joining());// "Iloveyou"http://String joined = stream.collect(Collectors.joining(","));// "I,love,you"Stringjoined=stream.collect(Collectors.joining(",","{","}"));// "{I,love,you}"
collect()還可以做更多
除了可以使用Collectors工具類已經(jīng)封裝好的收集器橄仍,我們還可以自定義收集器,或者直接調(diào)用collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)方法牍戚,收集任何形式你想要的信息侮繁。不過Collectors工具類應(yīng)該能滿足我們的絕大部分需求,手動實(shí)現(xiàn)之間請先看看文檔如孝。
Stream Pipelines
前面我們已經(jīng)學(xué)會如何使用Stream API宪哩,用起來真的很爽,但簡潔的方法下面似乎隱藏著無盡的秘密第晰,如此強(qiáng)大的API是如何實(shí)現(xiàn)的呢锁孟?比如Pipeline是怎么執(zhí)行的,每次方法調(diào)用都會導(dǎo)致一次迭代嗎但荤?自動并行又是怎么做到的罗岖,線程個數(shù)是多少?本節(jié)我們學(xué)習(xí)Stream流水線的原理腹躁,這是Stream實(shí)現(xiàn)的關(guān)鍵所在桑包。
首先回顧一下容器執(zhí)行Lambda表達(dá)式的方式,以ArrayList.forEach()方法為例纺非,具體代碼如下:
// ArrayList.forEach()publicvoidforEach(Consumer<?superE>action){...for(inti=0;modCount==expectedModCount&&i<size;i++){action.accept(elementData[i]);// 回調(diào)方法}...}
我們看到ArrayList.forEach()方法的主要邏輯就是一個for循環(huán)哑了,在該for循環(huán)里不斷調(diào)用action.accept()回調(diào)方法完成對元素的遍歷。這完全沒有什么新奇之處烧颖,回調(diào)方法在Java GUI的監(jiān)聽器中廣泛使用弱左。Lambda表達(dá)式的作用就是相當(dāng)于一個回調(diào)方法,這很好理解炕淮。
Stream API中大量使用Lambda表達(dá)式作為回調(diào)方法拆火,但這并不是關(guān)鍵。理解Stream我們更關(guān)心的是另外兩個問題:流水線和自動并行。使用Stream或許很容易寫入如下形式的代碼:
intlongestStringLengthStartingWithA=strings.stream().filter(s->s.startsWith("A")).mapToInt(String::length).max();
上述代碼求出以字母A開頭的字符串的最大長度们镜,一種直白的方式是為每一次函數(shù)調(diào)用都執(zhí)一次迭代币叹,這樣做能夠?qū)崿F(xiàn)功能,但效率上肯定是無法接受的模狭。類庫的實(shí)現(xiàn)著使用流水線(Pipeline)的方式巧妙的避免了多次迭代颈抚,其基本思想是在一次迭代中盡可能多的執(zhí)行用戶指定的操作。為講解方便我們匯總了Stream的所有操作嚼鹉。
Stream操作分類
中間操作(Intermediate operations)無狀態(tài)(Stateless)unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek()
有狀態(tài)(Stateful)distinct() sorted() sorted() limit() skip()
結(jié)束操作(Terminal operations)非短路操作forEach() forEachOrdered() toArray() reduce() collect() max() min() count()
短路操作(short-circuiting)anyMatch() allMatch() noneMatch() findFirst() findAny()
Stream上的所有操作分為兩類:中間操作和結(jié)束操作贩汉,中間操作只是一種標(biāo)記,只有結(jié)束操作才會觸發(fā)實(shí)際計(jì)算锚赤。中間操作又可以分為無狀態(tài)的(Stateless)和有狀態(tài)的(Stateful)匹舞,無狀態(tài)中間操作是指元素的處理不受前面元素的影響,而有狀態(tài)的中間操作必須等到所有元素處理之后才知道最終結(jié)果线脚,比如排序是有狀態(tài)操作策菜,在讀取所有元素之前并不能確定排序結(jié)果;結(jié)束操作又可以分為短路操作和非短路操作酒贬,短路操作是指不用處理全部元素就可以返回結(jié)果又憨,比如找到第一個滿足條件的元素。之所以要進(jìn)行如此精細(xì)的劃分锭吨,是因?yàn)榈讓訉γ恳环N情況的處理方式不同蠢莺。
一種直白的實(shí)現(xiàn)方式
仍然考慮上述求最長字符串的程序,一種直白的流水線實(shí)現(xiàn)方式是為每一次函數(shù)調(diào)用都執(zhí)一次迭代零如,并將處理中間結(jié)果放到某種數(shù)據(jù)結(jié)構(gòu)中(比如數(shù)組躏将,容器等)。具體說來考蕾,就是調(diào)用filter()方法后立即執(zhí)行祸憋,選出所有以A開頭的字符串并放到一個列表list1中,之后讓list1傳遞給mapToInt()方法并立即執(zhí)行肖卧,生成的結(jié)果放到list2中蚯窥,最后遍歷list2找出最大的數(shù)字作為最終結(jié)果。程序的執(zhí)行流程如如所示:
這樣做實(shí)現(xiàn)起來非常簡單直觀塞帐,但有兩個明顯的弊端:
迭代次數(shù)多拦赠。迭代次數(shù)跟函數(shù)調(diào)用的次數(shù)相等。
頻繁產(chǎn)生中間結(jié)果葵姥。每次函數(shù)調(diào)用都產(chǎn)生一次中間結(jié)果并蝗,存儲開銷無法接受铅乡。
這些弊端使得效率底下衡载,根本無法接受篓吁。如果不使用Stream API我們都知道上述代碼該如何在一次迭代中完成矮嫉,大致是如下形式:
intlongest=0;for(Stringstr:strings){if(str.startsWith("A")){// 1. filter(), 保留以A開頭的字符串intlen=str.length();// 2. mapToInt(), 轉(zhuǎn)換成長度longest=Math.max(len,longest);// 3. max(), 保留最長的長度}}
采用這種方式我們不但減少了迭代次數(shù),也避免了存儲中間結(jié)果牍疏,顯然這就是流水線敞临,因?yàn)槲覀儼讶齻€操作放在了一次迭代當(dāng)中。只要我們事先知道用戶意圖麸澜,總是能夠采用上述方式實(shí)現(xiàn)跟Stream API等價的功能,但問題是Stream類庫的設(shè)計(jì)者并不知道用戶的意圖是什么奏黑。如何在無法假設(shè)用戶行為的前提下實(shí)現(xiàn)流水線炊邦,是類庫的設(shè)計(jì)者要考慮的問題。
Stream流水線解決方案
我們大致能夠想到熟史,應(yīng)該采用某種方式記錄用戶每一步的操作馁害,當(dāng)用戶調(diào)用結(jié)束操作時將之前記錄的操作疊加到一起在一次迭代中全部執(zhí)行掉。沿著這個思路蹂匹,有幾個問題需要解決:
用戶的操作如何記錄碘菜?
操作如何疊加?
疊加之后的操作如何執(zhí)行限寞?
執(zhí)行后的結(jié)果(如果有)在哪里忍啸?
操作如何記錄?
注意這里使用的是“操作(operation)”一詞履植,指的是“Stream中間操作”的操作计雌,很多Stream操作會需要一個回調(diào)函數(shù)(Lambda表達(dá)式),因此一個完整的操作是<數(shù)據(jù)來源玫霎,操作凿滤,回調(diào)函數(shù)>構(gòu)成的三元組。Stream中使用Stage的概念來描述一個完整的操作庶近,并用某種實(shí)例化后的PipelineHelper來代表Stage翁脆,將具有先后順序的各個Stage連到一起,就構(gòu)成了整個流水線鼻种。跟Stream相關(guān)類和接口的繼承關(guān)系圖示反番。
還有IntPipeline, LongPipeline, DoublePipeline沒在圖中畫出,這三個類專門為三種基本類型(不是包裝類型)而定制的叉钥,跟ReferencePipeline是并列關(guān)系恬口。圖中Head用于表示第一個Stage,即調(diào)用調(diào)用諸如Collection.stream()方法產(chǎn)生的Stage沼侣,很顯然這個Stage里不包含任何操作祖能;StatelessOp和StatefulOp分別表示無狀態(tài)和有狀態(tài)的Stage,對應(yīng)于無狀態(tài)和有狀態(tài)的中間操作蛾洛。
Stream流水線組織結(jié)構(gòu)示意圖如下:
圖中通過Collection.stream()方法得到Head也就是stage0养铸,緊接著調(diào)用一系列的中間操作雁芙,不斷產(chǎn)生新的Stream。這些Stream對象以雙向鏈表的形式組織在一起钞螟,構(gòu)成整個流水線兔甘,由于每個Stage都記錄了前一個Stage和本次的操作以及回調(diào)函數(shù),依靠這種結(jié)構(gòu)就能建立起對數(shù)據(jù)源的所有操作鳞滨。這就是Stream記錄操作的方式洞焙。
操作如何疊加?
以上只是解決了操作記錄的問題拯啦,要想讓流水線起到應(yīng)有的作用我們需要一種將所有操作疊加到一起的方案澡匪。你可能會覺得這很簡單,只需要從流水線的head開始依次執(zhí)行每一步的操作(包括回調(diào)函數(shù))就行了褒链。這聽起來似乎是可行的唁情,但是你忽略了前面的Stage并不知道后面Stage到底執(zhí)行了哪種操作,以及回調(diào)函數(shù)是哪種形式甫匹。換句話說甸鸟,只有當(dāng)前Stage本身才知道該如何執(zhí)行自己包含的動作。這就需要有某種協(xié)議來協(xié)調(diào)相鄰Stage之間的調(diào)用關(guān)系兵迅。
這種協(xié)議由Sink接口完成抢韭,Sink接口包含的方法如下表所示:
方法名作用
void begin(long size)開始遍歷元素之前調(diào)用該方法,通知Sink做好準(zhǔn)備恍箭。
void end()所有元素遍歷完成之后調(diào)用篮绰,通知Sink沒有更多的元素了。
boolean cancellationRequested()是否可以結(jié)束操作季惯,可以讓短路操作盡早結(jié)束吠各。
void accept(T t)遍歷元素時調(diào)用,接受一個待處理元素勉抓,并對元素進(jìn)行處理贾漏。Stage把自己包含的操作和回調(diào)方法封裝到該方法里,前一個Stage只需要調(diào)用當(dāng)前Stage.accept(T t)方法就行了藕筋。
有了上面的協(xié)議纵散,相鄰Stage之間調(diào)用就很方便了,每個Stage都會將自己的操作封裝到一個Sink里隐圾,前一個Stage只需調(diào)用后一個Stage的accept()方法即可伍掀,并不需要知道其內(nèi)部是如何處理的。當(dāng)然對于有狀態(tài)的操作暇藏,Sink的begin()和end()方法也是必須實(shí)現(xiàn)的蜜笤。比如Stream.sorted()是一個有狀態(tài)的中間操作,其對應(yīng)的Sink.begin()方法可能創(chuàng)建一個乘放結(jié)果的容器盐碱,而accept()方法負(fù)責(zé)將元素添加到該容器把兔,最后end()負(fù)責(zé)對容器進(jìn)行排序沪伙。對于短路操作,Sink.cancellationRequested()也是必須實(shí)現(xiàn)的县好,比如Stream.findFirst()是短路操作围橡,只要找到一個元素,cancellationRequested()就應(yīng)該返回true缕贡,以便調(diào)用者盡快結(jié)束查找翁授。Sink的四個接口方法常常相互協(xié)作,共同完成計(jì)算任務(wù)晾咪。實(shí)際上Stream API內(nèi)部實(shí)現(xiàn)的的本質(zhì)收擦,就是如何重載Sink的這四個接口方法。
有了Sink對操作的包裝禀酱,Stage之間的調(diào)用問題就解決了,執(zhí)行時只需要從流水線的head開始對數(shù)據(jù)源依次調(diào)用每個Stage對應(yīng)的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了牧嫉。一種可能的Sink.accept()方法流程是這樣的:
voidaccept(Uu){1.使用當(dāng)前Sink包裝的回調(diào)函數(shù)處理u2.將處理結(jié)果傳遞給流水線下游的Sink}
Sink接口的其他幾個方法也是按照這種[處理->轉(zhuǎn)發(fā)]的模型實(shí)現(xiàn)剂跟。下面我們結(jié)合具體例子看看Stream的中間操作是如何將自身的操作包裝成Sink以及Sink是如何將處理結(jié)果轉(zhuǎn)發(fā)給下一個Sink的。先看Stream.map()方法:
// Stream.map()酣藻,調(diào)用該方法將產(chǎn)生一個新的Streampublicfinal<R>Stream<R>map(Function<?superP_OUT,?extendsR>mapper){...returnnewStatelessOp<P_OUT,R>(this,StreamShape.REFERENCE,StreamOpFlag.NOT_SORTED|StreamOpFlag.NOT_DISTINCT){@Override/*opWripSink()方法返回由回調(diào)函數(shù)包裝而成Sink*/Sink<P_OUT>opWrapSink(intflags,Sink<R>downstream){returnnewSink.ChainedReference<P_OUT,R>(downstream){@Overridepublicvoidaccept(P_OUTu){Rr=mapper.apply(u);// 1. 使用當(dāng)前Sink包裝的回調(diào)函數(shù)mapper處理udownstream.accept(r);// 2. 將處理結(jié)果傳遞給流水線下游的Sink}};}};}
上述代碼看似復(fù)雜曹洽,其實(shí)邏輯很簡單,就是將回調(diào)函數(shù)mapper包裝到一個Sink當(dāng)中辽剧。由于Stream.map()是一個無狀態(tài)的中間操作送淆,所以map()方法返回了一個StatelessOp內(nèi)部類對象(一個新的Stream),調(diào)用這個新Stream的opWripSink()方法將得到一個包裝了當(dāng)前回調(diào)函數(shù)的Sink怕轿。
再來看一個復(fù)雜一點(diǎn)的例子偷崩。Stream.sorted()方法將對Stream中的元素進(jìn)行排序,顯然這是一個有狀態(tài)的中間操作撞羽,因?yàn)樽x取所有元素之前是沒法得到最終順序的阐斜。拋開模板代碼直接進(jìn)入問題本質(zhì),sorted()方法是如何將操作封裝成Sink的呢诀紊?sorted()一種可能封裝的Sink代碼如下:
// Stream.sort()方法用到的Sink實(shí)現(xiàn)classRefSortingSink<T>extendsAbstractRefSortingSink<T>{privateArrayList<T>list;// 存放用于排序的元素RefSortingSink(Sink<?superT>downstream,Comparator<?superT>comparator){super(downstream,comparator);}@Overridepublicvoidbegin(longsize){...// 創(chuàng)建一個存放排序元素的列表list=(size>=0)?newArrayList<T>((int)size):newArrayList<T>();}@Overridepublicvoidend(){list.sort(comparator);// 只有元素全部接收之后才能開始排序downstream.begin(list.size());if(!cancellationWasRequested){// 下游Sink不包含短路操作list.forEach(downstream::accept);// 2. 將處理結(jié)果傳遞給流水線下游的Sink}else{// 下游Sink包含短路操作for(Tt:list){// 每次都調(diào)用cancellationRequested()詢問是否可以結(jié)束處理谒出。if(downstream.cancellationRequested())break;downstream.accept(t);// 2. 將處理結(jié)果傳遞給流水線下游的Sink}}downstream.end();list=null;}@Overridepublicvoidaccept(Tt){list.add(t);// 1. 使用當(dāng)前Sink包裝動作處理t,只是簡單的將元素添加到中間列表當(dāng)中}}
上述代碼完美的展現(xiàn)了Sink的四個接口方法是如何協(xié)同工作的:
首先beging()方法告訴Sink參與排序的元素個數(shù)邻奠,方便確定中間結(jié)果容器的的大畜栽;
之后通過accept()方法將元素添加到中間結(jié)果當(dāng)中碌宴,最終執(zhí)行時調(diào)用者會不斷調(diào)用該方法杀狡,直到遍歷所有元素;
最后end()方法告訴Sink所有元素遍歷完畢贰镣,啟動排序步驟捣卤,排序完成后將結(jié)果傳遞給下游的Sink忍抽;
如果下游的Sink是短路操作,將結(jié)果傳遞給下游時不斷詢問下游cancellationRequested()是否可以結(jié)束處理董朝。
疊加之后的操作如何執(zhí)行鸠项?
Sink完美封裝了Stream每一步操作,并給出了[處理->轉(zhuǎn)發(fā)]的模式來疊加操作子姜。這一連串的齒輪已經(jīng)咬合祟绊,就差最后一步撥動齒輪啟動執(zhí)行。是什么啟動這一連串的操作呢哥捕?也許你已經(jīng)想到了啟動的原始動力就是結(jié)束操作(Terminal Operation)牧抽,一旦調(diào)用某個結(jié)束操作,就會觸發(fā)整個流水線的執(zhí)行遥赚。
結(jié)束操作之后不能再有別的操作扬舒,所以結(jié)束操作不會創(chuàng)建新的流水線階段(Stage),直觀的說就是流水線的鏈表不會在往后延伸了凫佛。結(jié)束操作會創(chuàng)建一個包裝了自己操作的Sink讲坎,這也是流水線中最后一個Sink,這個Sink只需要處理數(shù)據(jù)而不需要將結(jié)果傳遞給下游的Sink(因?yàn)闆]有下游)愧薛。對于Sink的[處理->轉(zhuǎn)發(fā)]模型晨炕,結(jié)束操作的Sink就是調(diào)用鏈的出口。
我們再來考察一下上游的Sink是如何找到下游Sink的毫炉。一種可選的方案是在PipelineHelper中設(shè)置一個Sink字段瓮栗,在流水線中找到下游Stage并訪問Sink字段即可。但Stream類庫的設(shè)計(jì)者沒有這么做瞄勾,而是設(shè)置了一個Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法來得到Sink费奸,該方法的作用是返回一個新的包含了當(dāng)前Stage代表的操作以及能夠?qū)⒔Y(jié)果傳遞給downstream的Sink對象。為什么要產(chǎn)生一個新對象而不是返回一個Sink字段进陡?這是因?yàn)槭褂胦pWrapSink()可以將當(dāng)前操作與下游Sink(上文中的downstream參數(shù))結(jié)合成新Sink货邓。試想只要從流水線的最后一個Stage開始,不斷調(diào)用上一個Stage的opWrapSink()方法直到最開始(不包括stage0四濒,因?yàn)閟tage0代表數(shù)據(jù)源换况,不包含操作),就可以得到一個代表了流水線上所有操作的Sink盗蟆,用代碼表示就是這樣:
// AbstractPipeline.wrapSink()// 從下游向上游不斷包裝Sink戈二。如果最初傳入的sink代表結(jié)束操作,// 函數(shù)返回時就可以得到一個代表了流水線上所有操作的Sink喳资。final<P_IN>Sink<P_IN>wrapSink(Sink<E_OUT>sink){...for(AbstractPipelinep=AbstractPipeline.this;p.depth>0;p=p.previousStage){sink=p.opWrapSink(p.previousStage.combinedFlags,sink);}return(Sink<P_IN>)sink;}
現(xiàn)在流水線上從開始到結(jié)束的所有的操作都被包裝到了一個Sink里觉吭,執(zhí)行這個Sink就相當(dāng)于執(zhí)行整個流水線,執(zhí)行Sink的代碼如下:
// AbstractPipeline.copyInto(), 對spliterator代表的數(shù)據(jù)執(zhí)行wrappedSink代表的操作仆邓。final<P_IN>voidcopyInto(Sink<P_IN>wrappedSink,Spliterator<P_IN>spliterator){...if(!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())){wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知開始遍歷spliterator.forEachRemaining(wrappedSink);// 迭代wrappedSink.end();// 通知遍歷結(jié)束}...}
上述代碼首先調(diào)用wrappedSink.begin()方法告訴Sink數(shù)據(jù)即將到來鲜滩,然后調(diào)用spliterator.forEachRemaining()方法對數(shù)據(jù)進(jìn)行迭代(Spliterator是容器的一種迭代器伴鳖,參閱),最后調(diào)用wrappedSink.end()方法通知Sink數(shù)據(jù)處理結(jié)束徙硅。邏輯如此清晰榜聂。
執(zhí)行后的結(jié)果在哪里?
最后一個問題是流水線上所有操作都執(zhí)行后嗓蘑,用戶所需要的結(jié)果(如果有)在哪里须肆?首先要說明的是不是所有的Stream結(jié)束操作都需要返回結(jié)果,有些操作只是為了使用其副作用(Side-effects)桩皿,比如使用Stream.forEach()方法將結(jié)果打印出來就是常見的使用副作用的場景(事實(shí)上豌汇,除了打印之外其他場景都應(yīng)避免使用副作用),對于真正需要返回結(jié)果的結(jié)束操作結(jié)果存在哪里呢泄隔?
特別說明:副作用不應(yīng)該被濫用拒贱,也許你會覺得在Stream.forEach()里進(jìn)行元素收集是個不錯的選擇,就像下面代碼中那樣佛嬉,但遺憾的是這樣使用的正確性和效率都無法保證逻澳,因?yàn)镾tream可能會并行執(zhí)行。大多數(shù)使用副作用的地方都可以使用歸約操作更安全和有效的完成巷燥。
// 錯誤的收集方式ArrayList<String>results=newArrayList<>();stream.filter(s->pattern.matcher(s).matches()).forEach(s->results.add(s));// Unnecessary use of side-effects!// 正確的收集方式List<String>results=stream.filter(s->pattern.matcher(s).matches()).collect(Collectors.toList());// No side-effects!
回到流水線執(zhí)行結(jié)果的問題上來赡盘,需要返回結(jié)果的流水線結(jié)果存在哪里呢号枕?這要分不同的情況討論缰揪,下表給出了各種有返回結(jié)果的Stream結(jié)束操作。
返回類型對應(yīng)的結(jié)束操作
booleananyMatch() allMatch() noneMatch()
OptionalfindFirst() findAny()
歸約結(jié)果reduce() collect()
數(shù)組toArray()
對于表中返回boolean或者Optional的操作(Optional是存放 一個 值的容器)的操作葱淳,由于值返回一個值钝腺,只需要在對應(yīng)的Sink中記錄這個值,等到執(zhí)行結(jié)束時返回就可以了赞厕。
對于歸約操作艳狐,最終結(jié)果放在用戶調(diào)用時指定的容器中(容器類型通過收集器指定)。collect(), reduce(), max(), min()都是歸約操作皿桑,雖然max()和min()也是返回一個Optional毫目,但事實(shí)上底層是通過調(diào)用reduce()方法實(shí)現(xiàn)的。
對于返回是數(shù)組的情況诲侮,毫無疑問的結(jié)果會放在數(shù)組當(dāng)中镀虐。這么說當(dāng)然是對的,但在最終返回?cái)?shù)組之前沟绪,結(jié)果其實(shí)是存儲在一種叫做Node的數(shù)據(jù)結(jié)構(gòu)中的刮便。Node是一種多叉樹結(jié)構(gòu),元素存儲在樹的葉子當(dāng)中绽慈,并且一個葉子節(jié)點(diǎn)可以存放多個元素恨旱。這樣做是為了并行執(zhí)行方便辈毯。關(guān)于Node的具體結(jié)構(gòu),我們會在下一節(jié)探究Stream如何并行執(zhí)行時給出詳細(xì)說明搜贤。
本文詳細(xì)介紹了Stream流水線的組織方式和執(zhí)行過程谆沃,學(xué)習(xí)本文將有助于理解原理并寫出正確的Stream代碼,同時打消你對Stream API效率方面的顧慮入客。如你所見管毙,Stream API實(shí)現(xiàn)如此巧妙,即使我們使用外部迭代手動編寫等價代碼桌硫,也未必更加高效夭咬。
注:留下本文所用的JDK版本,以便有考究癖的人考證:
$ java-versionjava version"1.8.0_101"Java(TM)SE Runtime Environment(build 1.8.0_101-b13)Java HotSpot(TM)Server VM(build 25.101-b13, mixed mode)