Java?Lambda表達(dá)式的一個(gè)重要用法是簡(jiǎn)化某些匿名內(nèi)部類(Anonymous Classes)的寫(xiě)法。實(shí)際上Lambda表達(dá)式并不僅僅是匿名內(nèi)部類的語(yǔ)法糖,JVM內(nèi)部是通過(guò)invokedynamic指令來(lái)實(shí)現(xiàn)Lambda表達(dá)式的。具體原理放到下一篇。本篇我們首先感受一下使用Lambda表達(dá)式帶來(lái)的便利之處。
Lambda and Anonymous Classes(I)
本節(jié)將介紹如何使用Lambda表達(dá)式簡(jiǎn)化匿名內(nèi)部類的書(shū)寫(xiě),但Lambda表達(dá)式并不能取代所有的匿名內(nèi)部類,只能用來(lái)取代函數(shù)接口(Functional Interface)的簡(jiǎn)寫(xiě)。先別在乎細(xì)節(jié),看幾個(gè)例子再說(shuō)。
例子1:無(wú)參函數(shù)的簡(jiǎn)寫(xiě)
如果需要新建一個(gè)線程,一種常見(jiàn)的寫(xiě)法是這樣:
// JDK7 匿名內(nèi)部類寫(xiě)法newThread(newRunnable(){// 接口名@Overridepublicvoidrun(){// 方法名System.out.println("Thread run()");}}).start();
上述代碼給Tread類傳遞了一個(gè)匿名的Runnable對(duì)象,重載Runnable接口的run()方法來(lái)實(shí)現(xiàn)相應(yīng)邏輯。這是JDK7以及之前的常見(jiàn)寫(xiě)法。匿名內(nèi)部類省去了為類起名字的煩惱,但還是不夠簡(jiǎn)化,在Java 8中可以簡(jiǎn)化為如下形式:
// JDK8 Lambda表達(dá)式寫(xiě)法newThread(()->System.out.println("Thread run()")// 省略接口名和方法名).start();
上述代碼跟匿名內(nèi)部類的作用是一樣的,但比匿名內(nèi)部類更進(jìn)一步。這里連接口名和函數(shù)名都一同省掉了,寫(xiě)起來(lái)更加神清氣爽。如果函數(shù)體有多行,可以用大括號(hào)括起來(lái),就像這樣:
// JDK8 Lambda表達(dá)式代碼塊寫(xiě)法newThread(()->{System.out.print("Hello");System.out.println(" Hoolee");}).start();
例子2:帶參函數(shù)的簡(jiǎn)寫(xiě)
如果要給一個(gè)字符串列表通過(guò)自定義比較器,按照字符串長(zhǎng)度進(jìn)行排序,Java 7的書(shū)寫(xiě)形式如下:
// JDK7 匿名內(nèi)部類寫(xiě)法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();}});
上述代碼通過(guò)內(nèi)部類重載了Comparator接口的compare()方法,實(shí)現(xiàn)比較邏輯。采用Lambda表達(dá)式可簡(jiǎn)寫(xiě)如下:
// JDK8 Lambda表達(dá)式寫(xiě)法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í)候,這時(shí)就需要手動(dòng)指明參數(shù)類型了。注意,Java是強(qiáng)類型語(yǔ)言,每個(gè)變量和對(duì)象都必需有明確的類型。
簡(jiǎn)寫(xiě)的依據(jù)
也許你已經(jīng)想到了,能夠使用Lambda的依據(jù)是必須有相應(yīng)的函數(shù)接口(函數(shù)接口,是指內(nèi)部只有一個(gè)抽象方法的接口)。這一點(diǎn)跟Java是強(qiáng)類型語(yǔ)言吻合,也就是說(shuō)你并不能在代碼的任何地方任性的寫(xiě)Lambda表達(dá)式。實(shí)際上Lambda的類型就是對(duì)應(yīng)函數(shù)接口的類型。Lambda表達(dá)式另一個(gè)依據(jù)是類型推斷機(jī)制,在上下文信息足夠的情況下,編譯器可以推斷出參數(shù)表的類型,而不需要顯式指名。Lambda表達(dá)更多合法的書(shū)寫(xiě)形式如下:
// Lambda表達(dá)式的書(shū)寫(xiě)形式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展示了無(wú)參函數(shù)的簡(jiǎn)寫(xiě);2處展示了有參函數(shù)的簡(jiǎn)寫(xiě),以及類型推斷機(jī)制;3是代碼塊的寫(xiě)法;4和5再次展示了類型推斷機(jī)制。
自定義函數(shù)接口
自定義函數(shù)接口很容易,只需要編寫(xiě)一個(gè)只有一個(gè)抽象方法的接口即可。
// 自定義函數(shù)接口@FunctionalInterfacepublicinterfaceConsumerInterface<T>{voidaccept(Tt);}
上面代碼中的@FunctionalInterface是可選的,但加上該標(biāo)注編譯器會(huì)幫你檢查接口是否符合函數(shù)接口規(guī)范。就像加入@Override標(biāo)注會(huì)檢查是否重載了函數(shù)一樣。
有了上述接口定義,就可以寫(xiě)出類似如下的代碼:
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ù)接口書(shū)寫(xiě)Lambda表達(dá)式
Lambda and Anonymous Classes(II)
讀過(guò)上一篇之后,相信對(duì)Lambda表達(dá)式的語(yǔ)法以及基本原理有了一定了解。對(duì)于編寫(xiě)代碼,有這些知識(shí)已經(jīng)夠用。本文將進(jìn)一步區(qū)分Lambda表達(dá)式和匿名內(nèi)部類在JVM層面的區(qū)別,如果對(duì)這一部分不感興趣,可以跳過(guò)。
經(jīng)過(guò)第一篇的的介紹,我們看到Lambda表達(dá)式似乎只是為了簡(jiǎn)化匿名內(nèi)部類書(shū)寫(xiě),這看起來(lái)僅僅通過(guò)語(yǔ)法糖在編譯階段把所有的Lambda表達(dá)式替換成匿名內(nèi)部類就可以了。但實(shí)時(shí)并非如此。在JVM層面,Lambda表達(dá)式和匿名內(nèi)部類有著明顯的差別。
匿名內(nèi)部類實(shí)現(xiàn)
匿名內(nèi)部類仍然是一個(gè)類,只是不需要程序員顯示指定類名,編譯器會(huì)自動(dòng)為該類取名。因此如果有如下形式的代碼,編譯之后將會(huì)產(chǎn)生兩個(gè)class文件:
publicclassMainAnonymousClass{publicstaticvoidmain(String[]args){newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("Anonymous Class Thread run()");}}).start();;}}
編譯之后文件分布如下,兩個(gè)class文件分別是主類和匿名內(nèi)部類產(chǎn)生的:

進(jìn)一步分析主類MainAnonymousClass.class的字節(jié)碼,可發(fā)現(xiàn)其創(chuàng)建了匿名內(nèi)部類的對(duì)象:
// 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)部類對(duì)象*/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á)式通過(guò)invokedynamic指令實(shí)現(xiàn),書(shū)寫(xiě)Lambda表達(dá)式不會(huì)產(chǎn)生新的類。如果有如下代碼,編譯之后只有一個(gè)class文件:
publicclassMainLambda{publicstaticvoidmain(String[]args){newThread(()->System.out.println("Lambda Thread run()")).start();;}}
編譯之后的結(jié)果:

通過(guò)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á)式被封裝成了主類的一個(gè)私有方法,并通過(guò)invokedynamic指令進(jìn)行調(diào)用。
推論,this引用的意義
既然Lambda表達(dá)式不是內(nèi)部類的簡(jiǎn)寫(xiě),那么Lambda內(nèi)部的this引用也就跟內(nèi)部類對(duì)象沒(méi)什么關(guān)系了。在Lambda表達(dá)式中this的意義跟在表達(dá)式外部完全一樣。因此下列代碼將輸出兩遍Hello Hoolee,而不是兩個(gè)引用地址。
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)開(kāi)始說(shuō)起。
為引入Lambda表達(dá)式,Java8新增了java.util.funcion包,里面包含常用的函數(shù)接口,這是Lambda表達(dá)式的基礎(chǔ),Java集合框架也新增部分接口,以便與Lambda表達(dá)式對(duì)接。
首先回顧一下Java集合框架的接口繼承結(jié)構(gòu):

上圖中綠色標(biāo)注的接口類,表示在Java8中加入了新的接口方法,當(dāng)然由于繼承關(guān)系,他們相應(yīng)的子類也都會(huì)繼承這些新方法。下表詳細(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為例來(lái)說(shuō)明。了解Java7ArrayList實(shí)現(xiàn)原理,將有助于理解下文。
forEach()
該方法的簽名為void forEach(Consumer<? super E> action),作用是對(duì)容器中的每個(gè)元素執(zhí)行action指定的動(dòng)作,其中Consumer是個(gè)函數(shù)接口,里面只有一個(gè)待實(shí)現(xiàn)方法void accept(T t)(后面我們會(huì)看到,這個(gè)方法叫什么根本不重要,你甚至不需要記憶它的名字)。
需求:假設(shè)有一個(gè)字符串列表,需要打印出其中所有長(zhǎng)度大于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接口。到目前為止我們沒(méi)看到這種設(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()方法傳入一個(gè)Lambda表達(dá)式,我們不需要知道accept()方法,也不需要知道Consumer接口,類型推導(dǎo)幫我們做了一切。
removeIf()
該方法簽名為boolean removeIf(Predicate<? super E> filter),作用是刪除容器中所有滿足filter指定條件的元素,其中Predicate是一個(gè)函數(shù)接口,里面只有一個(gè)待實(shí)現(xiàn)方法boolean test(T t),同樣的這個(gè)方法的名字根本不重要,因?yàn)橛玫臅r(shí)候不需要書(shū)寫(xiě)這個(gè)名字。
需求:假設(shè)有一個(gè)字符串列表,需要?jiǎng)h除其中所有長(zhǎng)度大于3的字符串。
我們知道如果需要在迭代過(guò)程沖對(duì)容器進(jìn)行刪除操作必須使用迭代器,否則會(huì)拋出ConcurrentModificationException,所以上述任務(wù)傳統(tǒng)的寫(xiě)法是:
// 使用迭代器刪除列表元素ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));Iterator<String>it=list.iterator();while(it.hasNext()){if(it.next().length()>3)// 刪除長(zhǎng)度大于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>(){// 刪除長(zhǎng)度大于3的元素@Overridepublicbooleantest(Stringstr){returnstr.length()>3;}});
上述代碼使用removeIf()方法,并使用匿名內(nèi)部類實(shí)現(xiàn)Precicate接口。相信你已經(jīng)想到用Lambda表達(dá)式該怎么寫(xiě)了:
// 使用removeIf()結(jié)合Lambda表達(dá)式實(shí)現(xiàn)ArrayList<String>list=newArrayList<>(Arrays.asList("I","love","you","too"));list.removeIf(str->str.length()>3);// 刪除長(zhǎng)度大于3的元素
使用Lambda表達(dá)式不需要記憶Predicate接口名,也不需要記憶test()方法名,只需要知道此處需要一個(gè)返回布爾類型的Lambda表達(dá)式就行了。
replaceAll()
該方法簽名為void replaceAll(UnaryOperator<E> operator),作用是對(duì)每個(gè)元素執(zhí)行operator指定的操作,并用操作結(jié)果來(lái)替換原來(lái)的元素。其中UnaryOperator是一個(gè)函數(shù)接口,里面只有一個(gè)待實(shí)現(xiàn)函數(shù)T apply(T t)。
需求:假設(shè)有一個(gè)字符串列表,將其中所有長(zhǎng)度大于3的元素轉(zhuǎn)換成大寫(xiě),其余元素不變。
Java7及之前似乎沒(méi)有優(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接口。我們知道可以用更為簡(jiǎn)潔的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ī)則對(duì)容器元素進(jìn)行排序。Comparator接口我們并不陌生,其中有一個(gè)方法int compare(T o1, T o2)需要實(shí)現(xiàn),顯然該接口是個(gè)函數(shù)接口。
需求:假設(shè)有一個(gè)字符串列表,按照字符串長(zhǎng)度增序?qū)υ嘏判颉?/i>
由于Java7以及之前sort()方法在Collections工具類中,所以代碼要這樣寫(xiě):
// 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á)式,可以這樣寫(xiě):
// 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(),該方法返回容器的可拆分迭代器。從名字來(lái)看該方法跟iterator()方法有點(diǎn)像,我們知道Iterator是用來(lái)迭代容器的,Spliterator也有類似作用,但二者有如下不同:
Spliterator既可以像Iterator那樣逐個(gè)迭代,也可以批量迭代。批量迭代可以降低迭代的開(kāi)銷。
Spliterator是可拆分的,一個(gè)Spliterator可以通過(guò)調(diào)用Spliterator<T> trySplit()方法來(lái)嘗試分成兩個(gè)。一個(gè)是this,另一個(gè)是新返回的那個(gè),這兩個(gè)迭代器代表的元素沒(méi)有重疊。
可通過(guò)(多次)調(diào)用Spliterator.trySplit()方法來(lái)分解負(fù)載,以便多線程處理。
stream()和parallelStream()
stream()和parallelStream()分別返回該容器的Stream視圖表示,不同之處在于parallelStream()返回并行的Stream。Stream是Java函數(shù)式編程的核心類,我們會(huì)在后面章節(jié)中學(xué)習(xí)。
Map中的新方法
相比Collection,Map中加入了更多的方法,我們以HashMap為例來(lái)逐一探秘。了解Java7HashMap實(shí)現(xiàn)原理,將有助于理解下文。
forEach()
該方法簽名為void forEach(BiConsumer<? super K,? super V> action),作用是對(duì)Map中的每個(gè)映射執(zhí)行action指定的操作,其中BiConsumer是一個(gè)函數(shù)接口,里面有一個(gè)待實(shí)現(xiàn)方法void accept(T t, U u)。BinConsumer接口名字和accept()方法名字都不重要,請(qǐng)不要記憶他們。
需求:假設(shè)有一個(gè)數(shù)字到對(duì)應(yīng)英文單詞的Map,請(qǐng)輸出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í)際場(chǎng)景中沒(méi)人使用匿名內(nèi)部類寫(xiě)法,因?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中對(duì)應(yīng)的value,如果沒(méi)有找到則返回defaultValue。使用該方法程序員可以省去查詢指定鍵值是否存在的麻煩.
需求;假設(shè)有一個(gè)數(shù)字到對(duì)應(yīng)英文單詞的Map,輸出4對(duì)應(yīng)的英文單詞,如果不存在則輸出NoValue
// 查詢Map中指定的值,不存在時(shí)使用默認(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時(shí),才將value指定的值放入到Map中,否則不對(duì)Map做更改.該方法將條件判斷和賦值合二為一,使用起來(lái)更加方便.
remove()
我們都知道Map中有一個(gè)remove(Object key)方法,來(lái)根據(jù)指定key值刪除Map中的映射關(guān)系;Java8新增了remove(Object key, Object value)方法,只有在當(dāng)前Map中key正好映射到value時(shí)才刪除該映射,否則什么也不做.
replace()
在Java7及以前,要想替換Map中的映射關(guān)系可通過(guò)put(K key, V value)方法實(shí)現(xiàn),該方法總是會(huì)用新值替換原來(lái)的值.為了更精確的控制替換行為,Java8在Map中加入了兩個(gè)replace()方法,分別如下:
replace(K key, V value),只有在當(dāng)前Map中key的映射存在時(shí)才用value去替換原來(lái)的值,否則什么也不做.
replace(K key, V oldValue, V newValue),只有在當(dāng)前Map中key的映射存在且等于oldValue時(shí)才用newValue去替換原來(lái)的值,否則什么也不做.
replaceAll()
該方法簽名為replaceAll(BiFunction<? super K,? super V,? extends V> function),作用是對(duì)Map中的每個(gè)映射執(zhí)行function指定的操作,并用function的執(zhí)行結(jié)果替換原來(lái)的value,其中BiFunction是一個(gè)函數(shù)接口,里面有一個(gè)待實(shí)現(xiàn)方法R apply(T t, U u).不要被如此多的函數(shù)接口嚇到,因?yàn)槭褂玫臅r(shí)候根本不需要知道他們的名字.
需求:假設(shè)有一個(gè)數(shù)字到對(duì)應(yīng)英文單詞的Map,請(qǐng)將原來(lái)映射關(guān)系中的單詞都轉(zhuǎn)換成大寫(xiě).
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());
簡(jiǎn)潔到讓人難以置信.
merge()
該方法簽名為merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction),作用是:
如果Map中key對(duì)應(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)介紹過(guò),里面有一個(gè)待實(shí)現(xiàn)方法R apply(T t, U u).
merge()方法雖然語(yǔ)義有些復(fù)雜,但該方法的用方式很明確,一個(gè)比較常見(jiàn)的場(chǎng)景是將新的錯(cuò)誤信息拼接到原來(lái)的信息上,比如:
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()方法中錯(cuò)誤信息拼接的例子,使用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時(shí),才調(diào)用mappingFunction,并在mappingFunction執(zhí)行結(jié)果非null時(shí),將結(jié)果跟key關(guān)聯(lián).
Function是一個(gè)函數(shù)接口,里面有一個(gè)待實(shí)現(xiàn)方法R apply(T t).
computeIfAbsent()常用來(lái)對(duì)Map的某個(gè)key值建立初始化映射.比如我們要實(shí)現(xiàn)一個(gè)多值映射,Map的定義可能是Map<K,Set<V>>,要向Map中放入新值,可通過(guò)如下代碼實(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()將條件判斷和添加操作合二為一,使代碼更加簡(jiǎn)潔.
computeIfPresent()
該方法簽名為V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction),作用跟computeIfAbsent()相反,即,只有在當(dāng)前Map中存在key值的映射且非null時(shí),才調(diào)用remappingFunction,如果remappingFunction執(zhí)行結(jié)果為null,則刪除key的映射,否則使用該結(jié)果替換key原來(lái)的映射.
這個(gè)函數(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í)和使用這些方法有助于我們寫(xiě)出更加簡(jiǎn)潔有效的代碼.
函數(shù)接口雖然很多,但絕大多數(shù)時(shí)候我們根本不需要知道它們的名字,書(shū)寫(xiě)Lambda表達(dá)式時(shí)類型推斷幫我們做了一切.
Streams API(I)
你可能沒(méi)意識(shí)到Java對(duì)函數(shù)式編程的重視程度,看看Java 8加入函數(shù)式編程擴(kuò)充多少功能就清楚了。Java 8之所以費(fèi)這么大功夫引入函數(shù)式編程,原因有二:
代碼簡(jiǎn)潔函數(shù)式編程寫(xiě)出的代碼簡(jiǎn)潔且意圖明確,使用stream接口讓你從此告別for循環(huán)。
多核友好,Java函數(shù)式編程使得編寫(xiě)并行程序從未如此簡(jiǎn)單,你需要的全部就是調(diào)用一下parallel()方法。
這一節(jié)我們學(xué)習(xí)stream,也就是Java函數(shù)式編程的主角。對(duì)于Java 7來(lái)說(shuō)stream完全是個(gè)陌生東西,stream并不是某種數(shù)據(jù)結(jié)構(gòu),它只是數(shù)據(jù)源的一種視圖。這里的數(shù)據(jù)源可以是一個(gè)數(shù)組,Java容器或I/O channel等。正因如此要得到一個(gè)stream通常不會(huì)手動(dòng)創(chuàng)建,而是調(diào)用對(duì)應(yīng)的工具方法,比如:
調(diào)用Collection.stream()或者Collection.parallelStream()方法
調(diào)用Arrays.stream(T[] array)方法
常見(jiàn)的stream接口繼承關(guān)系如圖:

圖中4種stream接口繼承自BaseStream,其中IntStream, LongStream, DoubleStream對(duì)應(yīng)三種基本類型(int, long, double,注意不是包裝類型),Stream對(duì)應(yīng)所有剩余類型的stream視圖。為不同數(shù)據(jù)類型設(shè)置不同stream接口,可以1.提高性能,2.增加特定接口函數(shù)。

你可能會(huì)奇怪為什么不把IntStream等設(shè)計(jì)成Stream的子接口?畢竟這接口中的方法名大部分是一樣的。答案是這些方法的名字雖然相同,但是返回類型不同,如果設(shè)計(jì)成父子接口關(guān)系,這些方法將不能共存,因?yàn)镴ava不允許只有返回類型不同的方法重載。
雖然大部分情況下stream是容器調(diào)用Collection.stream()方法得到的,但stream和collections有以下不同:
無(wú)存儲(chǔ)。stream不是一種數(shù)據(jù)結(jié)構(gòu),它只是某種數(shù)據(jù)源的一個(gè)視圖,數(shù)據(jù)源可以是一個(gè)數(shù)組,Java容器或I/O channel等。
為函數(shù)式編程而生。對(duì)stream的任何修改都不會(huì)修改背后的數(shù)據(jù)源,比如對(duì)stream執(zhí)行過(guò)濾操作并不會(huì)刪除被過(guò)濾的元素,而是會(huì)產(chǎn)生一個(gè)不包含被過(guò)濾元素的新stream。
惰式執(zhí)行。stream上的操作并不會(huì)立即執(zhí)行,只有等到用戶真正需要結(jié)果的時(shí)候才會(huì)執(zhí)行。
可消費(fèi)性。stream只能被“消費(fèi)”一次,一旦遍歷過(guò)就會(huì)失效,就像容器的迭代器那樣,想要再次遍歷必須重新生成。
對(duì)stream的操作分為為兩類,中間操作(intermediate operations)和結(jié)束操作(terminal operations),二者特點(diǎn)是:
中間操作總是會(huì)惰式執(zhí)行,調(diào)用中間操作只會(huì)生成一個(gè)標(biāo)記了該操作的新stream,僅此而已。
結(jié)束操作會(huì)觸發(fā)實(shí)際計(jì)算,計(jì)算發(fā)生時(shí)會(huì)把所有中間操作積攢的操作以pipeline的方式執(zhí)行,這樣可以減少迭代次數(shù)。計(jì)算完成之后stream就會(huì)失效。
如果你熟悉Apache Spark RDD,對(duì)stream的這個(gè)特點(diǎn)應(yīng)該不陌生。
下表匯總了Stream接口的部分常見(jiàn)方法:
操作類型接口方法
中間操作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é)束操作最簡(jiǎn)單的方法,就是看方法的返回值,返回值為stream的大都是中間操作,否則是結(jié)束操作。
stream方法使用
stream跟函數(shù)接口關(guān)系非常緊密,沒(méi)有函數(shù)接口stream就無(wú)法工作?;仡櫼幌拢?b>函數(shù)接口是指內(nèi)部只有一個(gè)抽象方法的接口。通常函數(shù)接口出現(xiàn)的地方都可以使用Lambda表達(dá)式,所以不必記憶函數(shù)接口的名字。
forEach()
我們對(duì)forEach()方法并不陌生,在Collection中我們已經(jīng)見(jiàn)過(guò)。方法簽名為void forEach(Consumer<? super E> action),作用是對(duì)容器中的每個(gè)元素執(zhí)行action指定的動(dòng)作,也就是對(duì)元素進(jìn)行遍歷。
// 使用Stream.forEach()迭代Stream<String>stream=Stream.of("I","love","you","too");stream.forEach(str->System.out.println(str));
由于forEach()是結(jié)束方法,上述代碼會(huì)立即執(zhí)行,輸出所有字符串。
filter()

函數(shù)原型為Stream<T> filter(Predicate<? super T> predicate),作用是返回一個(gè)只包含滿足predicate條件元素的Stream。
// 保留長(zhǎng)度等于3的字符串Stream<String>stream=Stream.of("I","love","you","too");stream.filter(str->str.length()==3).forEach(str->System.out.println(str));
上述代碼將輸出為長(zhǎng)度等于3的字符串you和too。注意,由于filter()是個(gè)中間操作,如果只調(diào)用filter()不會(huì)有實(shí)際計(jì)算,因此也不會(huì)輸出任何信息。
distinct()

函數(shù)原型為Stream<T> distinct(),作用是返回一個(gè)去除重復(fù)元素之后的Stream。
Stream<String>stream=Stream.of("I","love","you","too","too");stream.distinct().forEach(str->System.out.println(str));
上述代碼會(huì)輸出去掉一個(gè)too之后的其余字符串。
sorted()
排序函數(shù)有兩個(gè),一個(gè)是用自然順序排序,一個(gè)是使用自定義比較器排序,函數(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));
上述代碼將輸出按照長(zhǎng)度升序排序后的字符串,結(jié)果完全在預(yù)料之中。
map()

函數(shù)原型為<R> Stream<R> map(Function<? super T,? extends R> mapper),作用是返回一個(gè)對(duì)當(dāng)前所有元素執(zhí)行執(zhí)行mapper之后的結(jié)果組成的Stream。直觀的說(shuō),就是對(duì)每個(gè)元素按照某種操作進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換前后Stream中元素的個(gè)數(shù)不會(huì)改變,但元素的類型取決于轉(zhuǎn)換之后的類型。
Stream<String>stream =Stream.of("I","love","you","too");stream.map(str->str.toUpperCase()).forEach(str->System.out.println(str));
上述代碼將輸出原字符串的大寫(xiě)形式。
flatMap()

函數(shù)原型為<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper),作用是對(duì)每個(gè)元素執(zhí)行mapper指定的操作,并用所有mapper返回的Stream中的元素組成一個(gè)新的Stream作為最終返回結(jié)果。說(shuō)起來(lái)太拗口,通俗的講flatMap()的作用就相當(dāng)于把原stream中的所有元素都”攤平”之后組成的Stream,轉(zhuǎn)換前后元素的個(gè)數(shù)和類型都可能會(huì)改變。
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));
上述代碼中,原來(lái)的stream中有兩個(gè)元素,分別是兩個(gè)List<Integer>,執(zhí)行flatMap()之后,將每個(gè)List都“攤平”成了一個(gè)個(gè)的數(shù)字,所以會(huì)新產(chǎn)生一個(gè)由5個(gè)數(shù)字組成的Stream。所以最終將輸出1~5這5個(gè)數(shù)字。
截止到目前我們感覺(jué)良好,已介紹Stream接口函數(shù)理解起來(lái)并不費(fèi)勁兒。如果你就此以為函數(shù)式編程不過(guò)如此,恐怕是高興地太早了。下一節(jié)對(duì)Stream規(guī)約操作的介紹將刷新你現(xiàn)在的認(rèn)識(shí)。
Streams API(II)
上一節(jié)介紹了部分Stream常見(jiàn)接口方法,理解起來(lái)并不困難,但Stream的用法不止于此,本節(jié)我們將仍然以Stream為例,介紹流的規(guī)約操作。
規(guī)約操作(reduction operation)又被稱作折疊操作(fold),是通過(guò)某個(gè)連接動(dòng)作將所有元素匯總成一個(gè)匯總結(jié)果的過(guò)程。元素求和、求最大值或最小值、求出元素總個(gè)數(shù)、將所有元素轉(zhuǎn)換成一個(gè)列表或集合,都屬于規(guī)約操作。Stream類庫(kù)有兩個(gè)通用的規(guī)約操作reduce()和collect(),也有一些為簡(jiǎn)化書(shū)寫(xiě)而設(shè)計(jì)的專用規(guī)約操作,比如sum()、max()、min()、count()等。
最大或最小值這類規(guī)約操作很好理解(至少方法語(yǔ)義上是這樣),我們著重介紹reduce()和collect(),這是比較有魔法的地方。
多面手reduce()
reduce操作可以實(shí)現(xiàn)從一組元素中生成一個(gè)值,sum()、max()、min()、count()等都是reduce操作,將他們單獨(dú)設(shè)為函數(shù)只是因?yàn)槌S?。reduce()的方法定義有三種重寫(xiě)形式:
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ù)定義越來(lái)越長(zhǎng),但語(yǔ)義不曾改變,多的參數(shù)只是為了指明初始值(參數(shù)identity),或者是指定并行執(zhí)行時(shí)多個(gè)部分結(jié)果的合并方式(參數(shù)combiner)。reduce()最常用的場(chǎng)景就是從一堆值中生成一個(gè)值。用這么復(fù)雜的函數(shù)去求一個(gè)最大或最小值,你是不是覺(jué)得設(shè)計(jì)者有病。其實(shí)不然,因?yàn)椤按蟆焙汀靶 被蛘摺扒蠛汀庇袝r(shí)會(huì)有不同的語(yǔ)義。
需求:從一組單詞中找出最長(zhǎng)的單詞。這里“大”的含義就是“長(zhǎng)”。
// 找出最長(zhǎng)的單詞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());
上述代碼會(huì)選出最長(zhǎng)的單詞love,其中Optional是(一個(gè))值的容器,使用它可以避免null值的麻煩。當(dāng)然可以使用Stream.max(Comparator<? super T> comparator)方法來(lái)達(dá)到同等效果,但reduce()自有其存在的理由。

需求:求出一組單詞的長(zhǎng)度之和。這是個(gè)“求和”操作,操作對(duì)象輸入類型是String,而結(jié)果類型是Integer。
// 求單詞長(zhǎng)度之和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í)行時(shí)才會(huì)用到 // (3)// int lengthSum = stream.mapToInt(str -> str.length()).sum();System.out.println(lengthSum);
上述代碼標(biāo)號(hào)(2)處將i. 字符串映射成長(zhǎng)度,ii. 并和當(dāng)前累加和相加。這顯然是兩步操作,使用reduce()函數(shù)將這兩步合二為一,更有助于提升性能。如果想要使用map()和sum()組合來(lái)達(dá)到上述目的,也是可以的。
reduce()擅長(zhǎng)的是生成一個(gè)值,如果想要從Stream生成一個(gè)集合或者Map等復(fù)雜的對(duì)象該怎么辦呢?終極武器collect()橫空出世!
終極武器collect()
不夸張的講,如果你發(fā)現(xiàn)某個(gè)功能在Stream接口中沒(méi)找到,十有八九可以通過(guò)collect()方法實(shí)現(xiàn)。collect()是Stream接口方法中最靈活的一個(gè),學(xué)會(huì)它才算真正入門(mén)Java函數(shù)式編程。先看幾個(gè)熱身的小例子:
// 將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。雖然代碼語(yǔ)義很明確,可是我們?nèi)匀粫?huì)有幾個(gè)疑問(wèn):
Function.identity()是干什么的?
String::length是什么意思?
Collectors是個(gè)什么東西?
接口的靜態(tài)方法和默認(rèn)方法
Function是一個(gè)接口,那么Function.identity()是什么意思呢?這要從兩方面解釋:
Java 8允許在接口中加入具體方法。接口中的具體方法有兩種,default方法和static方法,identity()就是Function接口的一個(gè)靜態(tài)方法。
Function.identity()返回一個(gè)輸出跟輸入一樣的Lambda表達(dá)式對(duì)象,等價(jià)于形如t -> t形式的Lambda表達(dá)式。
上面的解釋是不是讓你疑問(wèn)更多?不要問(wèn)我為什么接口中可以有具體方法,也不要告訴我你覺(jué)得t -> t比identity()方法更直觀。我會(huì)告訴你接口中的default方法是一個(gè)無(wú)奈之舉,在Java 7及之前要想在定義好的接口中加入新的抽象方法是很困難甚至不可能的,因?yàn)樗袑?shí)現(xiàn)了該接口的類都要重新實(shí)現(xiàn)。試想在Collection接口中加入一個(gè)stream()抽象方法會(huì)怎樣?default方法就是用來(lái)解決這個(gè)尷尬問(wèn)題的,直接在接口中實(shí)現(xiàn)新加入的方法。既然已經(jīng)引入了default方法,為何不再加入static方法來(lái)避免專門(mén)的工具類呢!
方法引用
諸如String::length的語(yǔ)法形式叫做方法引用(method references),這種語(yǔ)法用來(lái)替代某些特定形式Lambda表達(dá)式。如果Lambda表達(dá)式的全部?jī)?nèi)容就是調(diào)用一個(gè)已有的方法,那么可以用方法引用來(lái)替代Lambda表達(dá)式。方法引用可以細(xì)分為四類:
方法引用類別舉例
引用靜態(tài)方法Integer::sum
引用某個(gè)對(duì)象的方法list::add
引用某個(gè)類的方法String::length
引用構(gòu)造方法HashMap::new
我們會(huì)在后面的例子中使用方法引用。
收集器
相信前面繁瑣的內(nèi)容已徹底打消了你學(xué)習(xí)Java函數(shù)式編程的熱情,不過(guò)很遺憾,下面的內(nèi)容更繁瑣。但這不能怪Stream類庫(kù),因?yàn)橐獙?shí)現(xiàn)的功能本身很復(fù)雜。

收集器(Collector)是為Stream.collect()方法量身打造的工具接口(類)??紤]一下將一個(gè)Stream轉(zhuǎn)換成一個(gè)容器(或者Map)需要做哪些工作?我們至少需要兩樣?xùn)|西:
目標(biāo)容器是什么?是ArrayList還是HashSet,或者是個(gè)TreeMap。
新元素如何添加到容器中?是List.add()還是Map.put()。
如果并行的進(jìn)行規(guī)約,還需要告訴collect()?3. 多個(gè)部分結(jié)果如何合并成一個(gè)。
結(jié)合以上分析,collect()方法定義為<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三個(gè)參數(shù)依次對(duì)應(yīng)上述三條分析。不過(guò)每次調(diào)用collect()都要傳入這三個(gè)參數(shù)太麻煩,收集器Collector就是對(duì)這三個(gè)參數(shù)的簡(jiǎn)單封裝,所以collect()的另一定義為<R,A> R collect(Collector<? super T,A,R> collector)。Collectors工具類可通過(guò)靜態(tài)方法生成各種常用的Collector。舉例來(lái)說(shuō),如果要將Stream規(guī)約成List可以通過(guò)如下兩種方式實(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);
通常情況下我們不需要手動(dòng)指定collect()的三個(gè)參數(shù),而是調(diào)用collect(Collector<? super T,A,R> collector)方法,并且參數(shù)中的Collector對(duì)象大都是直接通過(guò)Collectors工具類獲得。實(shí)際上傳入的收集器的行為決定了collect()的行為。
使用collect()生成Collection
前面已經(jīng)提到通過(guò)collect()方法將Stream轉(zhuǎn)換成容器的方法,這里再匯總一下。將Stream轉(zhuǎn)換成List或Set是比較常見(jiàn)的操作,所以Collectors工具已經(jīng)為我們提供了對(duì)應(yīng)的收集器,通過(guò)如下代碼即可完成:
// 將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é)果是接口類型,我們并不知道類庫(kù)實(shí)際選擇的容器類型是什么,有時(shí)候我們可能會(huì)想要人為指定容器的實(shí)際類型,這個(gè)需求可通過(guò)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)說(shuō)過(guò)Stream背后依賴于某種數(shù)據(jù)源,數(shù)據(jù)源可以是數(shù)組、容器等,但不能是Map。反過(guò)來(lái)從Stream生成Map是可以的,但我們要想清楚Map的key和value分別代表什么,根本原因是我們要想清楚要干什么。通常在三種情況下collect()的結(jié)果會(huì)是Map:
使用Collectors.toMap()生成的收集器,用戶需要指定如何生成Map的key和value。
使用Collectors.partitioningBy()生成的收集器,對(duì)元素進(jìn)行二分區(qū)操作時(shí)用到。
使用Collectors.groupingBy()生成的收集器,對(duì)元素做group操作時(shí)用到。
情況1:使用toMap()生成的收集器,這種情況是最直接的,前面例子中已提到,這是和Collectors.toCollection()并列的方法。如下代碼展示將學(xué)生列表轉(zhuǎn)換成由<學(xué)生,GPA>組成的Map。非常直觀,無(wú)需多言。
// 使用toMap()統(tǒng)計(jì)學(xué)生GPAMap<Student,Double>studentToGPA=students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成keystudent->computeGPA(student)));// 如何生成value
情況2:使用partitioningBy()生成的收集器,這種情況適用于將Stream中的元素依據(jù)某個(gè)二值邏輯(滿足條件,或不滿足)分成互補(bǔ)相交的兩部分,比如男女性別、成績(jī)及格與否等。下列代碼展示將學(xué)生分成成績(jī)及格或不及格的兩部分。
// 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語(yǔ)句類似,這里的groupingBy()也是按照某個(gè)屬性對(duì)數(shù)據(jù)進(jìn)行分組,屬性相同的元素會(huì)被對(duì)應(yīng)到Map的同一個(gè)key上。下列代碼展示將員工按照部門(mén)進(jìn)行分組:
// Group employees by departmentMap<Department,List<Employee>>byDept=employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));
以上只是分組的最基本用法,有些時(shí)候僅僅分組是不夠的。在SQL中使用group by是為了協(xié)助其他查詢,比如1. 先將員工按照部門(mén)分組,2. 然后統(tǒng)計(jì)每個(gè)部門(mén)員工的人數(shù)。Java類庫(kù)設(shè)計(jì)者也考慮到了這種情況,增強(qiáng)版的groupingBy()能夠滿足這種需求。增強(qiáng)版的groupingBy()允許我們對(duì)元素分組之后再執(zhí)行某種運(yùn)算,比如求和、計(jì)數(shù)、平均值、類型轉(zhuǎn)換等。這種先將元素分組的收集器叫做上游收集器,之后執(zhí)行其他運(yùn)算的收集器叫做下游收集器(downstream Collector)。
// 使用下游收集器統(tǒng)計(jì)每個(gè)部門(mén)的人數(shù)Map<Department,Integer>totalByDept=employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.counting()));// 下游收集器
上面代碼的邏輯是不是越看越像SQL?高度非結(jié)構(gòu)化。還有更狠的,下游收集器還可以包含更下游的收集器,這絕不是為了炫技而增加的把戲,而是實(shí)際場(chǎng)景需要??紤]將員工按照部門(mén)分組的場(chǎng)景,如果我們想得到每個(gè)員工的名字(字符串),而不是一個(gè)個(gè)Employee對(duì)象,可通過(guò)如下方式做到:
// 按照部門(mén)對(duì)員工分布組,并只保留員工的名字Map<Department,List<String>>byDept=employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.mapping(Employee::getName,// 下游收集器Collectors.toList())));// 更下游的收集器
如果看到這里你還沒(méi)有對(duì)Java函數(shù)式編程失去信心,恭喜你,你已經(jīng)順利成為Java函數(shù)式編程大師了。
使用collect()做字符串join
這個(gè)肯定是大家喜聞樂(lè)見(jiàn)的功能,字符串拼接時(shí)使用Collectors.joining()生成的收集器,從此告別for循環(huán)。Collectors.joining()方法有三種重寫(xiě)形式,分別對(duì)應(yīng)三種不同的拼接方式。無(wú)需多言,代碼過(guò)目難忘。
// 使用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)方法,收集任何形式你想要的信息。不過(guò)Collectors工具類應(yīng)該能滿足我們的絕大部分需求,手動(dòng)實(shí)現(xiàn)之間請(qǐng)先看看文檔。
Stream Pipelines
前面我們已經(jīng)學(xué)會(huì)如何使用Stream API,用起來(lái)真的很爽,但簡(jiǎn)潔的方法下面似乎隱藏著無(wú)盡的秘密,如此強(qiáng)大的API是如何實(shí)現(xiàn)的呢?比如Pipeline是怎么執(zhí)行的,每次方法調(diào)用都會(huì)導(dǎo)致一次迭代嗎?自動(dòng)并行又是怎么做到的,線程個(gè)數(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()方法的主要邏輯就是一個(gè)for循環(huán),在該for循環(huán)里不斷調(diào)用action.accept()回調(diào)方法完成對(duì)元素的遍歷。這完全沒(méi)有什么新奇之處,回調(diào)方法在Java GUI的監(jiān)聽(tīng)器中廣泛使用。Lambda表達(dá)式的作用就是相當(dāng)于一個(gè)回調(diào)方法,這很好理解。
Stream API中大量使用Lambda表達(dá)式作為回調(diào)方法,但這并不是關(guān)鍵。理解Stream我們更關(guān)心的是另外兩個(gè)問(wèn)題:流水線和自動(dòng)并行。使用Stream或許很容易寫(xiě)入如下形式的代碼:
intlongestStringLengthStartingWithA=strings.stream().filter(s->s.startsWith("A")).mapToInt(String::length).max();
上述代碼求出以字母A開(kāi)頭的字符串的最大長(zhǎng)度,一種直白的方式是為每一次函數(shù)調(diào)用都執(zhí)一次迭代,這樣做能夠?qū)崿F(xiàn)功能,但效率上肯定是無(wú)法接受的。類庫(kù)的實(shí)現(xiàn)著使用流水線(Pipeline)的方式巧妙的避免了多次迭代,其基本思想是在一次迭代中盡可能多的執(zhí)行用戶指定的操作。為講解方便我們匯總了Stream的所有操作。
Stream操作分類
中間操作(Intermediate operations)無(wú)狀態(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é)束操作才會(huì)觸發(fā)實(shí)際計(jì)算。中間操作又可以分為無(wú)狀態(tài)的(Stateless)和有狀態(tài)的(Stateful),無(wú)狀態(tài)中間操作是指元素的處理不受前面元素的影響,而有狀態(tài)的中間操作必須等到所有元素處理之后才知道最終結(jié)果,比如排序是有狀態(tài)操作,在讀取所有元素之前并不能確定排序結(jié)果;結(jié)束操作又可以分為短路操作和非短路操作,短路操作是指不用處理全部元素就可以返回結(jié)果,比如找到第一個(gè)滿足條件的元素。之所以要進(jìn)行如此精細(xì)的劃分,是因?yàn)榈讓訉?duì)每一種情況的處理方式不同。
一種直白的實(shí)現(xiàn)方式

仍然考慮上述求最長(zhǎng)字符串的程序,一種直白的流水線實(shí)現(xiàn)方式是為每一次函數(shù)調(diào)用都執(zhí)一次迭代,并將處理中間結(jié)果放到某種數(shù)據(jù)結(jié)構(gòu)中(比如數(shù)組,容器等)。具體說(shuō)來(lái),就是調(diào)用filter()方法后立即執(zhí)行,選出所有以A開(kāi)頭的字符串并放到一個(gè)列表list1中,之后讓list1傳遞給mapToInt()方法并立即執(zhí)行,生成的結(jié)果放到list2中,最后遍歷list2找出最大的數(shù)字作為最終結(jié)果。程序的執(zhí)行流程如如所示:
這樣做實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單直觀,但有兩個(gè)明顯的弊端:
迭代次數(shù)多。迭代次數(shù)跟函數(shù)調(diào)用的次數(shù)相等。
頻繁產(chǎn)生中間結(jié)果。每次函數(shù)調(diào)用都產(chǎn)生一次中間結(jié)果,存儲(chǔ)開(kāi)銷無(wú)法接受。
這些弊端使得效率底下,根本無(wú)法接受。如果不使用Stream API我們都知道上述代碼該如何在一次迭代中完成,大致是如下形式:
intlongest=0;for(Stringstr:strings){if(str.startsWith("A")){// 1. filter(), 保留以A開(kāi)頭的字符串intlen=str.length();// 2. mapToInt(), 轉(zhuǎn)換成長(zhǎng)度longest=Math.max(len,longest);// 3. max(), 保留最長(zhǎng)的長(zhǎng)度}}
采用這種方式我們不但減少了迭代次數(shù),也避免了存儲(chǔ)中間結(jié)果,顯然這就是流水線,因?yàn)槲覀儼讶齻€(gè)操作放在了一次迭代當(dāng)中。只要我們事先知道用戶意圖,總是能夠采用上述方式實(shí)現(xiàn)跟Stream API等價(jià)的功能,但問(wèn)題是Stream類庫(kù)的設(shè)計(jì)者并不知道用戶的意圖是什么。如何在無(wú)法假設(shè)用戶行為的前提下實(shí)現(xiàn)流水線,是類庫(kù)的設(shè)計(jì)者要考慮的問(wèn)題。
Stream流水線解決方案
我們大致能夠想到,應(yīng)該采用某種方式記錄用戶每一步的操作,當(dāng)用戶調(diào)用結(jié)束操作時(shí)將之前記錄的操作疊加到一起在一次迭代中全部執(zhí)行掉。沿著這個(gè)思路,有幾個(gè)問(wèn)題需要解決:
用戶的操作如何記錄?
操作如何疊加?
疊加之后的操作如何執(zhí)行?
執(zhí)行后的結(jié)果(如果有)在哪里?
操作如何記錄?

注意這里使用的是“操作(operation)”一詞,指的是“Stream中間操作”的操作,很多Stream操作會(huì)需要一個(gè)回調(diào)函數(shù)(Lambda表達(dá)式),因此一個(gè)完整的操作是<數(shù)據(jù)來(lái)源,操作,回調(diào)函數(shù)>構(gòu)成的三元組。Stream中使用Stage的概念來(lái)描述一個(gè)完整的操作,并用某種實(shí)例化后的PipelineHelper來(lái)代表Stage,將具有先后順序的各個(gè)Stage連到一起,就構(gòu)成了整個(gè)流水線。跟Stream相關(guān)類和接口的繼承關(guān)系圖示。
還有IntPipeline, LongPipeline, DoublePipeline沒(méi)在圖中畫(huà)出,這三個(gè)類專門(mén)為三種基本類型(不是包裝類型)而定制的,跟ReferencePipeline是并列關(guān)系。圖中Head用于表示第一個(gè)Stage,即調(diào)用調(diào)用諸如Collection.stream()方法產(chǎn)生的Stage,很顯然這個(gè)Stage里不包含任何操作;StatelessOp和StatefulOp分別表示無(wú)狀態(tài)和有狀態(tài)的Stage,對(duì)應(yīng)于無(wú)狀態(tài)和有狀態(tài)的中間操作。
Stream流水線組織結(jié)構(gòu)示意圖如下:

圖中通過(guò)Collection.stream()方法得到Head也就是stage0,緊接著調(diào)用一系列的中間操作,不斷產(chǎn)生新的Stream。這些Stream對(duì)象以雙向鏈表的形式組織在一起,構(gòu)成整個(gè)流水線,由于每個(gè)Stage都記錄了前一個(gè)Stage和本次的操作以及回調(diào)函數(shù),依靠這種結(jié)構(gòu)就能建立起對(duì)數(shù)據(jù)源的所有操作。這就是Stream記錄操作的方式。
操作如何疊加?
以上只是解決了操作記錄的問(wèn)題,要想讓流水線起到應(yīng)有的作用我們需要一種將所有操作疊加到一起的方案。你可能會(huì)覺(jué)得這很簡(jiǎn)單,只需要從流水線的head開(kāi)始依次執(zhí)行每一步的操作(包括回調(diào)函數(shù))就行了。這聽(tīng)起來(lái)似乎是可行的,但是你忽略了前面的Stage并不知道后面Stage到底執(zhí)行了哪種操作,以及回調(diào)函數(shù)是哪種形式。換句話說(shuō),只有當(dāng)前Stage本身才知道該如何執(zhí)行自己包含的動(dòng)作。這就需要有某種協(xié)議來(lái)協(xié)調(diào)相鄰Stage之間的調(diào)用關(guān)系。
這種協(xié)議由Sink接口完成,Sink接口包含的方法如下表所示:
方法名作用
void begin(long size)開(kāi)始遍歷元素之前調(diào)用該方法,通知Sink做好準(zhǔn)備。
void end()所有元素遍歷完成之后調(diào)用,通知Sink沒(méi)有更多的元素了。
boolean cancellationRequested()是否可以結(jié)束操作,可以讓短路操作盡早結(jié)束。
void accept(T t)遍歷元素時(shí)調(diào)用,接受一個(gè)待處理元素,并對(duì)元素進(jìn)行處理。Stage把自己包含的操作和回調(diào)方法封裝到該方法里,前一個(gè)Stage只需要調(diào)用當(dāng)前Stage.accept(T t)方法就行了。
有了上面的協(xié)議,相鄰Stage之間調(diào)用就很方便了,每個(gè)Stage都會(huì)將自己的操作封裝到一個(gè)Sink里,前一個(gè)Stage只需調(diào)用后一個(gè)Stage的accept()方法即可,并不需要知道其內(nèi)部是如何處理的。當(dāng)然對(duì)于有狀態(tài)的操作,Sink的begin()和end()方法也是必須實(shí)現(xiàn)的。比如Stream.sorted()是一個(gè)有狀態(tài)的中間操作,其對(duì)應(yīng)的Sink.begin()方法可能創(chuàng)建一個(gè)乘放結(jié)果的容器,而accept()方法負(fù)責(zé)將元素添加到該容器,最后end()負(fù)責(zé)對(duì)容器進(jìn)行排序。對(duì)于短路操作,Sink.cancellationRequested()也是必須實(shí)現(xiàn)的,比如Stream.findFirst()是短路操作,只要找到一個(gè)元素,cancellationRequested()就應(yīng)該返回true,以便調(diào)用者盡快結(jié)束查找。Sink的四個(gè)接口方法常常相互協(xié)作,共同完成計(jì)算任務(wù)。實(shí)際上Stream API內(nèi)部實(shí)現(xiàn)的的本質(zhì),就是如何重載Sink的這四個(gè)接口方法。
有了Sink對(duì)操作的包裝,Stage之間的調(diào)用問(wèn)題就解決了,執(zhí)行時(shí)只需要從流水線的head開(kāi)始對(duì)數(shù)據(jù)源依次調(diào)用每個(gè)Stage對(duì)應(yīng)的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了。一種可能的Sink.accept()方法流程是這樣的:
voidaccept(Uu){1.使用當(dāng)前Sink包裝的回調(diào)函數(shù)處理u2.將處理結(jié)果傳遞給流水線下游的Sink}
Sink接口的其他幾個(gè)方法也是按照這種[處理->轉(zhuǎn)發(fā)]的模型實(shí)現(xiàn)。下面我們結(jié)合具體例子看看Stream的中間操作是如何將自身的操作包裝成Sink以及Sink是如何將處理結(jié)果轉(zhuǎn)發(fā)給下一個(gè)Sink的。先看Stream.map()方法:
// Stream.map(),調(diào)用該方法將產(chǎn)生一個(gè)新的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í)邏輯很簡(jiǎn)單,就是將回調(diào)函數(shù)mapper包裝到一個(gè)Sink當(dāng)中。由于Stream.map()是一個(gè)無(wú)狀態(tài)的中間操作,所以map()方法返回了一個(gè)StatelessOp內(nèi)部類對(duì)象(一個(gè)新的Stream),調(diào)用這個(gè)新Stream的opWripSink()方法將得到一個(gè)包裝了當(dāng)前回調(diào)函數(shù)的Sink。
再來(lái)看一個(gè)復(fù)雜一點(diǎn)的例子。Stream.sorted()方法將對(duì)Stream中的元素進(jìn)行排序,顯然這是一個(gè)有狀態(tài)的中間操作,因?yàn)樽x取所有元素之前是沒(méi)法得到最終順序的。拋開(kāi)模板代碼直接進(jìn)入問(wè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)建一個(gè)存放排序元素的列表list=(size>=0)?newArrayList<T>((int)size):newArrayList<T>();}@Overridepublicvoidend(){list.sort(comparator);// 只有元素全部接收之后才能開(kāi)始排序downstream.begin(list.size());if(!cancellationWasRequested){// 下游Sink不包含短路操作list.forEach(downstream::accept);// 2. 將處理結(jié)果傳遞給流水線下游的Sink}else{// 下游Sink包含短路操作for(Tt:list){// 每次都調(diào)用cancellationRequested()詢問(wèn)是否可以結(jié)束處理。if(downstream.cancellationRequested())break;downstream.accept(t);// 2. 將處理結(jié)果傳遞給流水線下游的Sink}}downstream.end();list=null;}@Overridepublicvoidaccept(Tt){list.add(t);// 1. 使用當(dāng)前Sink包裝動(dòng)作處理t,只是簡(jiǎn)單的將元素添加到中間列表當(dāng)中}}
上述代碼完美的展現(xiàn)了Sink的四個(gè)接口方法是如何協(xié)同工作的:
首先beging()方法告訴Sink參與排序的元素個(gè)數(shù),方便確定中間結(jié)果容器的的大?。?/p>
之后通過(guò)accept()方法將元素添加到中間結(jié)果當(dāng)中,最終執(zhí)行時(shí)調(diào)用者會(huì)不斷調(diào)用該方法,直到遍歷所有元素;
最后end()方法告訴Sink所有元素遍歷完畢,啟動(dòng)排序步驟,排序完成后將結(jié)果傳遞給下游的Sink;
如果下游的Sink是短路操作,將結(jié)果傳遞給下游時(shí)不斷詢問(wèn)下游cancellationRequested()是否可以結(jié)束處理。
疊加之后的操作如何執(zhí)行?

Sink完美封裝了Stream每一步操作,并給出了[處理->轉(zhuǎn)發(fā)]的模式來(lái)疊加操作。這一連串的齒輪已經(jīng)咬合,就差最后一步撥動(dòng)齒輪啟動(dòng)執(zhí)行。是什么啟動(dòng)這一連串的操作呢?也許你已經(jīng)想到了啟動(dòng)的原始動(dòng)力就是結(jié)束操作(Terminal Operation),一旦調(diào)用某個(gè)結(jié)束操作,就會(huì)觸發(fā)整個(gè)流水線的執(zhí)行。
結(jié)束操作之后不能再有別的操作,所以結(jié)束操作不會(huì)創(chuàng)建新的流水線階段(Stage),直觀的說(shuō)就是流水線的鏈表不會(huì)在往后延伸了。結(jié)束操作會(huì)創(chuàng)建一個(gè)包裝了自己操作的Sink,這也是流水線中最后一個(gè)Sink,這個(gè)Sink只需要處理數(shù)據(jù)而不需要將結(jié)果傳遞給下游的Sink(因?yàn)闆](méi)有下游)。對(duì)于Sink的[處理->轉(zhuǎn)發(fā)]模型,結(jié)束操作的Sink就是調(diào)用鏈的出口。
我們?cè)賮?lái)考察一下上游的Sink是如何找到下游Sink的。一種可選的方案是在PipelineHelper中設(shè)置一個(gè)Sink字段,在流水線中找到下游Stage并訪問(wèn)Sink字段即可。但Stream類庫(kù)的設(shè)計(jì)者沒(méi)有這么做,而是設(shè)置了一個(gè)Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法來(lái)得到Sink,該方法的作用是返回一個(gè)新的包含了當(dāng)前Stage代表的操作以及能夠?qū)⒔Y(jié)果傳遞給downstream的Sink對(duì)象。為什么要產(chǎn)生一個(gè)新對(duì)象而不是返回一個(gè)Sink字段?這是因?yàn)槭褂胦pWrapSink()可以將當(dāng)前操作與下游Sink(上文中的downstream參數(shù))結(jié)合成新Sink。試想只要從流水線的最后一個(gè)Stage開(kāi)始,不斷調(diào)用上一個(gè)Stage的opWrapSink()方法直到最開(kāi)始(不包括stage0,因?yàn)閟tage0代表數(shù)據(jù)源,不包含操作),就可以得到一個(gè)代表了流水線上所有操作的Sink,用代碼表示就是這樣:
// AbstractPipeline.wrapSink()// 從下游向上游不斷包裝Sink。如果最初傳入的sink代表結(jié)束操作,// 函數(shù)返回時(shí)就可以得到一個(gè)代表了流水線上所有操作的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)在流水線上從開(kāi)始到結(jié)束的所有的操作都被包裝到了一個(gè)Sink里,執(zhí)行這個(gè)Sink就相當(dāng)于執(zhí)行整個(gè)流水線,執(zhí)行Sink的代碼如下:
// AbstractPipeline.copyInto(), 對(duì)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());// 通知開(kāi)始遍歷spliterator.forEachRemaining(wrappedSink);// 迭代wrappedSink.end();// 通知遍歷結(jié)束}...}
上述代碼首先調(diào)用wrappedSink.begin()方法告訴Sink數(shù)據(jù)即將到來(lái),然后調(diào)用spliterator.forEachRemaining()方法對(duì)數(shù)據(jù)進(jìn)行迭代(Spliterator是容器的一種迭代器,參閱),最后調(diào)用wrappedSink.end()方法通知Sink數(shù)據(jù)處理結(jié)束。邏輯如此清晰。
執(zhí)行后的結(jié)果在哪里?
最后一個(gè)問(wèn)題是流水線上所有操作都執(zhí)行后,用戶所需要的結(jié)果(如果有)在哪里?首先要說(shuō)明的是不是所有的Stream結(jié)束操作都需要返回結(jié)果,有些操作只是為了使用其副作用(Side-effects),比如使用Stream.forEach()方法將結(jié)果打印出來(lái)就是常見(jiàn)的使用副作用的場(chǎng)景(事實(shí)上,除了打印之外其他場(chǎng)景都應(yīng)避免使用副作用),對(duì)于真正需要返回結(jié)果的結(jié)束操作結(jié)果存在哪里呢?
特別說(shuō)明:副作用不應(yīng)該被濫用,也許你會(huì)覺(jué)得在Stream.forEach()里進(jìn)行元素收集是個(gè)不錯(cuò)的選擇,就像下面代碼中那樣,但遺憾的是這樣使用的正確性和效率都無(wú)法保證,因?yàn)镾tream可能會(huì)并行執(zhí)行。大多數(shù)使用副作用的地方都可以使用歸約操作更安全和有效的完成。
// 錯(cuò)誤的收集方式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é)果的問(wèn)題上來(lái),需要返回結(jié)果的流水線結(jié)果存在哪里呢?這要分不同的情況討論,下表給出了各種有返回結(jié)果的Stream結(jié)束操作。
返回類型對(duì)應(yīng)的結(jié)束操作
booleananyMatch() allMatch() noneMatch()
OptionalfindFirst() findAny()
歸約結(jié)果reduce() collect()
數(shù)組toArray()
對(duì)于表中返回boolean或者Optional的操作(Optional是存放 一個(gè) 值的容器)的操作,由于值返回一個(gè)值,只需要在對(duì)應(yīng)的Sink中記錄這個(gè)值,等到執(zhí)行結(jié)束時(shí)返回就可以了。
對(duì)于歸約操作,最終結(jié)果放在用戶調(diào)用時(shí)指定的容器中(容器類型通過(guò)收集器指定)。collect(), reduce(), max(), min()都是歸約操作,雖然max()和min()也是返回一個(gè)Optional,但事實(shí)上底層是通過(guò)調(diào)用reduce()方法實(shí)現(xiàn)的。
對(duì)于返回是數(shù)組的情況,毫無(wú)疑問(wèn)的結(jié)果會(huì)放在數(shù)組當(dāng)中。這么說(shuō)當(dāng)然是對(duì)的,但在最終返回?cái)?shù)組之前,結(jié)果其實(shí)是存儲(chǔ)在一種叫做Node的數(shù)據(jù)結(jié)構(gòu)中的。Node是一種多叉樹(shù)結(jié)構(gòu),元素存儲(chǔ)在樹(shù)的葉子當(dāng)中,并且一個(gè)葉子節(jié)點(diǎn)可以存放多個(gè)元素。這樣做是為了并行執(zhí)行方便。關(guān)于Node的具體結(jié)構(gòu),我們會(huì)在下一節(jié)探究Stream如何并行執(zhí)行時(shí)給出詳細(xì)說(shuō)明。
本文詳細(xì)介紹了Stream流水線的組織方式和執(zhí)行過(guò)程,學(xué)習(xí)本文將有助于理解原理并寫(xiě)出正確的Stream代碼,同時(shí)打消你對(duì)Stream API效率方面的顧慮。如你所見(jiàn),Stream API實(shí)現(xiàn)如此巧妙,即使我們使用外部迭代手動(dòng)編寫(xiě)等價(jià)代碼,也未必更加高效。
注:留下本文所用的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)