本章節(jié)主要介紹lambda表達式簡化匿名內(nèi)部類的寫法首繁,但lambda表達式不能取代所有的匿名內(nèi)部類滚局,只能用來取代函數(shù)接口的簡寫。
Collection中的新方法
forEach()
該方法簽名為void forEach(Consumer<? super E> action),作用是對容器中的每個元素執(zhí)行action指定的動作辜贵,其中Consumer是個函數(shù)接口,里面只有一個待實現(xiàn)方法void accept(T t)归形。
需求:假設(shè)有一個字符串列表托慨,需要打印出其中所有長度大于3的字符串.
public static void main(String[] args) {
List<String> list = Arrays.asList("i", "love", "you");
//for循環(huán)
for (String s : list){
if (s.length() > 3){
System.out.println(s);
}
}
//forEach結(jié)合匿名內(nèi)部類
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
if (s.length() > 3){
System.out.println(s);
}
}
});
//forEach+lambda表達式寫法
list.forEach(s -> {
if (s.length() > 3){
System.out.println(s);
}
});
}
lambda表示中,我們不需要指定accept()方法暇榴,也不需要知道Consumer接口厚棵,類型推導(dǎo)幫我們做了一切。
removeIf()
該方法簽名為boolean removeIf(Predicate<? super E> filter)蔼紧,作用是刪除容器中所有滿足filter指定條件的元素窟感,其中Predicate是一個函數(shù)接口,里面只有一個待實現(xiàn)方法boolean test(T t)歉井。
需求:假設(shè)有一個字符串列表柿祈,需要刪除其中所有長度大于3的字符串。
我們知道如果需要在迭代過程中對容器進行刪除操作必須使用迭代器,否則會拋出ConcurrentModificationException躏嚎,所以上述任務(wù)傳統(tǒng)的寫法是:
ArrayList<String> list1 = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
//刪除長度大于3的
//迭代器
Iterator<String> iterator = list1.iterator();
while (iterator.hasNext()){
if (iterator.next().length() > 3){
iterator.remove();
}
}
//匿名類+removeIf
list1.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() > 3;
}
});
//lambda + removeIf
list1.removeIf( s -> s.length() >3 );
使用Lambda表達式不需要記憶Predicate接口名蜜自,也不需要記憶test()方法名,只需要知道此處需要一個返回布爾類型的Lambda表達式就行了卢佣。
這里需要注意:List<String> list = Arrays.asList("i", "love", "you")里實際new ArrayList()實際上為Arrays里的內(nèi)部類ArrayList重荠,該內(nèi)部類并未實現(xiàn)remove方法,所有此處不能直接用 List<String> list = Arrays.asList("i", "love", "you")虚茶,而是需要用ArrayList<String> list1 = new ArrayList<>(Arrays.asList("I", "love", "you", "too"))
replaceAll()
該方法簽名為void replaceAll(UnaryOperator<E> operator)戈鲁,作用是對每個元素執(zhí)行operator指定的操作,并用操作結(jié)果來替換原來的元素嘹叫。其中UnaryOperator是一個函數(shù)接口婆殿,里面只有一個待實現(xiàn)函數(shù)T apply(T t)。
需求:假設(shè)有一個字符串列表罩扇,將其中所有長度大于3的元素轉(zhuǎn)換成大寫婆芦,其余元素不變。
//假設(shè)有一個字符串列表喂饥,將其中所有長度大于3的元素轉(zhuǎn)換成大寫消约,其余元素不變。
//傳統(tǒng)寫法
for (int i=0; i < list.size(); i++) {
if (list.get(i).length() > 3){
list.set(i, list.get(i).toUpperCase());
}
}
//匿名類+replaceAll
list.replaceAll(new UnaryOperator<String>() {
@Override
public String apply(String s) {
if (s.length() > 3){
return s.toUpperCase();
}
return s;
}
});
//lambda+replaceAll
list.replaceAll( s -> {
if (s.length() > 3){
return s.toUpperCase();
}
return s;
});
sort()
該方法定義在List接口中员帮,方法簽名為void sort(Comparator<? super E> c)或粮,該方法根據(jù)c指定的比較規(guī)則對容器元素進行排序。Comparator接口我們并不陌生捞高,其中有一個方法int compare(T o1, T o2)需要實現(xiàn)氯材,顯然該接口是個函數(shù)接口。
需求:假設(shè)有一個字符串列表棠枉,按照字符串長度增序?qū)υ嘏判颉?/p>
//需求:假設(shè)有一個字符串列表浓体,按照字符串長度增序?qū)υ嘏判颉? //collection.sort
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
//lambda
list.sort((o1, o2) -> o1.length() - o2.length());
spliterator()
方法簽名為Spliterator<E> spliterator(),該方法返回容器的可拆分迭代器辈讶。從名字來看該方法跟iterator()方法有點像命浴,我們知道Iterator是用來迭代容器的,Spliterator也有類似作用贱除,但二者有如下不同:
- Spliterator既可以像Iterator那樣逐個迭代生闲,也可以批量迭代。批量迭代可以降低迭代的開銷月幌。
- Splierator是可拆分的碍讯,一個Spliterator可以通過調(diào)用Spliterator<T> trySplit()方法來嘗試分成兩個。一個是this扯躺,另一個是新返回的哪個捉兴,這兩個迭代器代表的元素沒有重疊蝎困。
可通過(多次)調(diào)用Spliterator.trySplit()方法來分解負載,以便多線程處理倍啥。
stream()和parallelStream()
stream()和parallelStream()分表返回該容器的Stream視圖表示禾乘,不同之處在于parallelStream()返回并行的Stream。Stream是Java函數(shù)式編程的核心類虽缕,后續(xù)章節(jié)中會學(xué)習(xí)到始藕。
Map中的新方法
forEach
該方法簽名為void forEach(BiConsumer<? super K,? super V> action),作用是對Map中的每個映射執(zhí)行action指定的操作氮趋,其中BiConsumer是一個函數(shù)接口伍派,里面有一個待實現(xiàn)方法void accept(T t, U u)。BinConsumer接口名字和accept()方法名字都不重要剩胁,請不要記憶他們诉植。
需求:假設(shè)有一個數(shù)字到對應(yīng)英文單詞的Map,請輸出Map中的所有映射關(guān)系
/java7及其以前的寫法
Map<Integer, String> map = new HashMap<>();
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());
}
//內(nèi)部類+forEach
map.forEach(new BiConsumer<Integer, String>() {
@Override
public void accept(Integer integer, String s) {
System.out.println(integer + "," + s);
}
});
//lambda+forEach
map.forEach(((integer, s) -> System.out.println(integer + "," + s)));
getOrDefault
該方法跟Lambda表達式?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
//需求潮罪;假設(shè)有一個數(shù)字到對應(yīng)英文單詞的Map康谆,輸出4對應(yīng)的英文單詞,如果不存在則輸出NoValue
if (map.containsKey(4)){
System.out.println(map.get(4));
} else {
System.out.println("NoValue");
}
//lambda
System.out.println(map.getOrDefault(4, "NoValue"));
putIfAbsent
該方法跟Lambda表達式?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)方法實現(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ù)接口铐望,里面有一個待實現(xiàn)方法R apply(T t, U u)冈涧。
需求:假設(shè)有一個數(shù)字對應(yīng)英文單詞的Map,請將原本映射關(guān)系中的單詞都轉(zhuǎn)換成大寫正蛙。
// Java7以及之前替換所有Map中所有映射關(guān)系
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
//Java7以及之前替換所有Map中所有映射關(guān)系
for (Map.Entry<Integer, String> entry : map.entrySet()){
entry.setValue(entry.getValue().toUpperCase());
}
//使用replaceAll()方法結(jié)合匿名內(nèi)部類
map.replaceAll(new BiFunction<Integer, String, String>() {
@Override
public String apply(Integer integer, String s) {
return s.toUpperCase();
}
});
//lambda
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)介紹過狂塘,里面有一個待實現(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的計算結(jié)果關(guān)聯(lián)到key上,如果計算結(jié)果為null新思,則在Map中刪除key的映射窖梁。
要實現(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ù)接口瞬捕,里面有一個待實現(xiàn)方法R apply(T t)。
computeIfAbsent()常用來對Map的某個key值建立初始化映射.比如我們要實現(xiàn)一個多值映射舵抹,Map的定義可能是Map<K,Set<V>>肪虎,要向Map中放入新值,可通過如下代碼實現(xiàn):
Map<Integer, Set<String>> map = new HashMap<>();
// Java7及以前的實現(xiàn)方式
if(map.containsKey(1)){
map.get(1).add("one");
}else{
Set<String> valueSet = new HashSet<String>();
valueSet.add("one");
map.put(1, valueSet);
}
// Java8的實現(xiàn)方式
map.computeIfAbsent(1, v -> new HashSet<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原來的映射像云。
// Java7及以前跟computeIfPresent()等效的代碼
if (map.get(key) != null) {
V oldValue = map.get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null)
map.put(key, newValue);
else
map.remove(key);
return newValue;
}
return null;
- Java8為容器新增一些有用的方法锌雀,這些方法有些是為完善原有功能,有些是為引入函數(shù)式編程迅诬,學(xué)習(xí)和使用這些方法有助于我們寫出更加簡潔有效的代碼腋逆。
- 函數(shù)接口雖然很多,但絕大多數(shù)時候我們根本不需要知道它們的名字百框,書寫Lambda表達式時類型推斷幫我們做了一切闲礼。
Streams API(I)
Java8之所以費這么大功夫引入函數(shù)式編程牍汹,原因有:
- 代碼簡潔铐维,函數(shù)式編程寫出的代碼簡潔且意圖明確,使用stream接口讓你從此告別for循環(huán)慎菲。
- 多核友好嫁蛇,Java函數(shù)式編程使得編寫并行程序從未如此簡單,你需要的全部就是調(diào)用一下parallel()方法露该。
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è)計成Stream的子接口稻轨?畢竟這接口中的方法名大部分是一樣的。答案是這些方法的名字雖然相同雕凹,但是返回類型不同殴俱,如果設(shè)計成父子接口關(guān)系,這些方法將不能共存枚抵,因為Java不允許只有返回類型不同的方法重載粱挡。
雖然大部分情況下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í)行。
- 可消費性廓鞠。stream只能被“消費”一次帚稠,一旦遍歷過就會失效,就像容器的迭代器那樣床佳,想要再次遍歷必須重新生成滋早。
對stream的操作分為兩類,中間操作和結(jié)束操作砌们,二者特點是:
- 中間操作總是會惰性執(zhí)行杆麸,調(diào)用中間操作只會生成一個標記了該操作的新stream,僅此而已浪感。
- 結(jié)束操作會觸發(fā)實際計算昔头,計算發(fā)生時會把所有中間操作積攢的操作以pipeline的方式執(zhí)行,這樣可以減少迭代次數(shù)影兽。計算完成之后stream就會失效揭斧。
下表匯總了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表達式纸型,所以不必記憶函數(shù)接口的名字拇砰。
forEach
我們對forEach()方法并不陌生,在Collection中我們已經(jīng)見過狰腌。方法簽名為void forEach(Consumer<? super E> action)饲握,作用是對容器中的每個元素執(zhí)行action指定的動作哪雕,也就是對元素進行遍歷文搂。
Stream stream = Stream.of("I", "test", "stream");
stream.forEach(str -> System.out.println(str));
由于forEach()是結(jié)束方法斟叼,上述方法會立即執(zhí)行,輸出所有字符串丹莲。
filter
函數(shù)原型為Stream<T> filter(Predicate<? super T> predicate)光坝,作用是返回一個只包含滿足predicate條件元素的Stream。
Stream<String> stream1 = Stream.of("I", "abc", "stream");
stream1.filter(s -> s.length() == 3)
.forEach(s -> System.out.println(s));
上述代碼將輸出長度為3的字符串a(chǎn)bc甥材。注意盯另,由于filter()是個中間操作,如果只調(diào)用filter()不會有實際計算洲赵,因此也不會輸出任何信息鸳惯。
distinct
函數(shù)原型為Stream<T> distinct(),作用是返回一個去重后的Stream叠萍。
Stream<String> stream= Stream.of("I", "I", "you", "too", "too");
stream.distinct()
.forEach(str -> System.out.println(str));
sorted()
排序函數(shù)有兩個芝发,一個是用自然順序排序,一個是使用自定義比較器排序俭令,函數(shù)原型分別為Stream<T> sorted()和Stream<T> sorted(Comparator<? super T> comparator)后德。
Stream<String> stream2 = Stream.of("a", "abc", "ab");
stream2.sorted((s1, s2) -> s1.length() - s2.length())
.forEach(s -> System.out.println(s));
map()
函數(shù)原型為<R> Stream<R> map(Function<? super T, ? extends R> mapper),作用是返回一個對當(dāng)前所有元素執(zhí)行mapper之后的結(jié)果組成的Stream抄腔。直觀的說,就是對每個元素按照某種操作進行轉(zhuǎn)換理张,轉(zhuǎn)換前后Stream中元素的個數(shù)不會改變赫蛇,但元素的類型取決于轉(zhuǎn)換之后的類型。
Stream<String> stream3 = Stream.of("a", "abc", "ab");
stream3.map(s -> s.toUpperCase())
.forEach(s -> System.out.println(s));
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>> stream4 = Stream.of(Arrays.asList(1, 2, 3), Arrays.asList(4, 5, 6));
stream4.flatMap(list -> list.stream())
.forEach(s -> System.out.println(s));
上述代碼中,原來的stream中有兩個元素旺嬉,分別是兩個List<Integer>管行,執(zhí)行flatMap()之后,將每個List都“攤平”成了一個個的數(shù)字邪媳,所以會新產(chǎn)生一個由6個數(shù)字組成的Stream捐顷。所以最終將輸出1~6。
截止到目前我們感覺良好雨效,已介紹Stream接口函數(shù)理解起來并不費勁兒迅涮。如果你就此以為函數(shù)式編程不過如此,恐怕是高興地太早了徽龟。下一節(jié)對Stream規(guī)約操作的介紹將刷新你現(xiàn)在的認識叮姑。
Streams API(II)
上一節(jié)介紹了部分Stream常見接口方法,理解起來并不困難据悔,但Stream的用法不止于此戏溺,本節(jié)我們將仍然以Stream為例,介紹流的規(guī)約操作屠尊。
規(guī)約操作又被稱作折疊操作旷祸,是通過某個連接動作將所有元素匯總成一個匯總結(jié)果的過程。元素求和讼昆、求最大值或最小值托享、求出元素總個數(shù)、將所有元素轉(zhuǎn)換成一個列表或集合浸赫,都屬于規(guī)約操作闰围。Stream類庫有兩個通用的規(guī)約操作reduce()和collect(),也有一些為簡化書寫而設(shè)計的專用規(guī)約操作既峡,比如sum()羡榴、max()、min()运敢、count()等校仑。
最大或最小值這類的規(guī)約操作很好理解,我們著重介紹reduce()和collect()传惠,這是比較有魔法的地方迄沫。
多面手reduce()
reduce操作可以實現(xiàn)一組元素中生成一個值,sum()卦方、max()羊瘩、min()、count()等都是reduce操作,將他們單獨設(shè)為函數(shù)只是因為常用尘吗。reduce()的方法定義有三種重寫形式:
- 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è)計者有病宁昭。其實不然,因為“大”和“小”或者“求和”有時會有不同的語義酗宋。
需求:從一組單詞中找出最長的單詞积仗。這里“大”的含義就是“長”。
Stream<String> stream5 = Stream.of("a", "abc", "ab");
Optional<String> longest = stream5.reduce(((s1, s2) -> s1.length() >= s2.length() ? s1 :s2));
//或
Optional<String> longest1 = stream5.max((s1, s2) -> s1.length() - s2.length());
System.out.println(longest.get());
上述代碼會選出最長的abc蜕猫,其中Optional是(一個)值的容器寂曹,使用它可以避免null值得麻煩。當(dāng)然可以使用Stream.max(Comparator<? super T> comparator)方法來達到同等效果回右,但reduce()自有其存在的理由隆圆。
需求:求出一組單詞的長度之和。這是個“求和”操作翔烁,操作對象輸入類型是String渺氧,而結(jié)果類型是Integer。
// 求單詞長度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = 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);
上述代碼標號(2)處
- 將字符串映射成長度
- 并和當(dāng)前累加和相加
者顯然是兩步操作侣背,使用reduce()函數(shù)將者兩步合二為一,更有助于提升性能慨默。如果想要使用map()和sum()組合來達到上述目的贩耐,也是可以的。
上述代碼標號(3)處實際上是(1)+(2)得到的結(jié)果厦取。
reduce()擅長的是生成一個值潮太,如果想要從Stream生成一個集合或者Map等復(fù)雜對象該怎么辦呢?終極武器collect()橫空出世虾攻!
終極武器collect()
不夸張的講铡买,如果你發(fā)現(xiàn)某個功能在Stream接口中沒找到,十有八九可以通過collect()方法實現(xiàn)台谢。collect()是Stream接口方法中最靈活的一個寻狂,學(xué)會它才算是真正入門Java函數(shù)式編程。
先來幾個熱身的小例子:
// 將Stream轉(zhuǎn)換成容器或Map
Stream<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.identiy()是干什么的樊拓?
- String::length是什么意思纠亚?
- Collectors是什么東西?
接口的靜態(tài)方法和默認方法
Function是一個接口筋夏,那么Function.identity()是什么意思蒂胞?這要從兩方面解釋:
- Java8允許在接口中加入具體方法。接口中的具體方法有兩種条篷,default方法和static方法骗随,identity()就是Function接口的一個靜態(tài)方法。
- Function.identity()返回一個輸出跟輸入一樣的Lambda表達式對象赴叹,等價于形如t -> t形式的Lambda表達式鸿染。
上面的解釋是不是讓你疑問更多?不要問我為什么接口中可以有具體方法乞巧,也不要告訴我你覺得t -> t比identity()方法更直觀涨椒。我會告訴你接口中的default方法是一個無奈之舉,在Java 7及之前要想在定義好的接口中加入新的抽象方法是很困難甚至不可能的绽媒,因為所有實現(xiàn)了該接口的類都要重新實現(xiàn)蚕冬。試想在Collection接口中加入一個stream()抽象方法會怎樣?default方法就是用來解決這個尷尬問題的是辕,直接在接口中實現(xiàn)新加入的方法囤热。既然已經(jīng)引入了default方法,為何不再加入static方法來避免專門的工具類呢获三!
方法引用
諸如String::length的語法形式叫做方法引用(method references)旁蔼,這種語法用來替代某些特定形式Lambda表達式。如果Lambda表達式的全部內(nèi)容就是調(diào)用一個已有的方法石窑,那么可以用方法引用來替代Lambda表達式牌芋。方法引用可以細分為四類。
方法引用類別 | 舉例 |
---|---|
引用靜態(tài)方法 | Interger::sum |
引用某個對象的方法 | list::add |
引用某個類的方法 | String::length |
引用構(gòu)造方法 | HashMap::new |
收集器
相信前面繁瑣的內(nèi)容已徹底打消了你學(xué)習(xí)Java函數(shù)式編程的熱情松逊,不過很遺憾躺屁,下面的內(nèi)容更繁瑣。但這不能怪Stream類庫经宏,因為要實現(xiàn)的功能本身很復(fù)雜犀暑。
收集器(Collector)是為Stream.collect()方法量身打造的工具接口(類)∷咐迹考慮一下將一個stream轉(zhuǎn)換成一個容器(或者Map)需要做哪些工作耐亏?我們至少需要兩樣?xùn)|西:
- 目標容器是什么?是ArrayList還是HashSet沪斟,或者是個TreeMap广辰。
- 新元素如何添加到容器中?是List.add()還是Map.put()。
- 如果并行的進行規(guī)約择吊,還需要告訴collect()多個部分結(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可以通過如下兩種方式實現(xiàn):
// 將Stream規(guī)約成List
Stream<String> stream = Stream.of("a", "ab", "abc", "abdc");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);
通常情況下我們不需要手動指定collect()的三個參數(shù)纷妆,而是調(diào)用collect(Collector<? super T,A,R> collector)方法,并且參數(shù)中的Collector對象大都是直接通過Collectors工具類獲得吼蚁。實際上傳入的收集器的行為決定了collect()的行為凭需。
使用collect()生成Collection
前面已經(jīng)提到通過collect()方法將Stream轉(zhuǎn)換成容器的方法,這里再匯總一下肝匆。將Stream轉(zhuǎn)換成List或Set是比較常見的操作粒蜈,所有Collections工具已經(jīng)為我們提供了對應(yīng)的收集器,通過如下代碼即可完成:
// 將Stream轉(zhuǎn)換成List或Set
Stream<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é)果是接口類型枯怖,我們并不知道類庫實際選擇的容器類型是什么,有時候我們可能會想要人為指定容器的實際類型能曾,這個需求可通過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()生成的收集器,對元素進行二分區(qū)操作時用到惭婿。
- 使用Collectors.groupingBy()生成的收集器不恭,對元素做group操作時用到叶雹。
情況1:使用toMap()生成的收集器,這種情況是最直接的县袱,前面例子中已提到浑娜,這是和Collectors.toCollection()并列的方法佑力。如下代碼展示將學(xué)生列表轉(zhuǎn)換成由<學(xué)生式散,GPA>組成的Map。非常直觀打颤,無需多言暴拄。
// 使用toMap()統(tǒng)計學(xué)生GPA
Map<Student, Double> studentToGPA =
students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key
student -> computeGPA(student)));// 如何生成value
情況2:使用partitioningBy()生成的收集器,這種情況適用于將Stream中的元素依據(jù)某個二值邏輯(滿足條件编饺,或不滿足)分成互補相交的兩部分乖篷,比如男女性別、成績及格與否等透且。下列代碼展示將學(xué)生分成成績及格或不及格的兩部分撕蔼。
// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
情況3:使用groupingBy()生成的收集器,這是比較靈活的一種情況秽誊。跟SQL中的group by語句類似鲸沮,這里的groupingBy()也是按照某個屬性對數(shù)據(jù)進行分組,屬性相同的元素會被對應(yīng)到Map的同一個key上锅论。下列代碼展示將員工按照部門進行分組:
// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment));
以上只是分組的最基本用法讼溺,有些時候僅僅分組是不夠的。在SQL中使用group by是為了協(xié)助其他查詢最易,比如1. 先將員工按照部門分組怒坯,2. 然后統(tǒng)計每個部門員工的人數(shù)。Java類庫設(shè)計者也考慮到了這種情況藻懒,增強版的groupingBy()能夠滿足這種需求剔猿。增強版的groupingBy()允許我們對元素分組之后再執(zhí)行某種運算,比如求和嬉荆、計數(shù)归敬、平均值、類型轉(zhuǎn)換等员寇。這種先將元素分組的收集器叫做上游收集器弄慰,之后執(zhí)行其他運算的收集器叫做下游收集器(downstream Collector)。
// 使用下游收集器統(tǒng)計每個部門的人數(shù)
Map<Department, Integer> totalByDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.counting()));// 下游收集器
上面代碼的邏輯是不是越看越像SQL蝶锋?高度非結(jié)構(gòu)化陆爽。還有更狠的,下游收集器還可以包含更下游的收集器扳缕,這絕不是為了炫技而增加的把戲慌闭,而是實際場景需要别威。考慮將員工按照部門分組的場景驴剔,如果我們想得到每個員工的名字(字符串)省古,而不是一個個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"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = 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)該能滿足我們的絕大部分需求卖怜,手動實現(xiàn)之間請先看看文檔屎开。
Stream Pipelines
前面我們已經(jīng)學(xué)會如何使用Stream API,用起來真的很爽马靠,但簡潔的方法下面似乎隱藏著無盡的秘密奄抽,如此強大的API是如何實現(xiàn)的呢?比如Pipeline是怎么執(zhí)行的甩鳄,每次方法調(diào)用都會導(dǎo)致一次迭代嗎逞度?自動并行又是怎么做到的,線程個數(shù)是多少妙啃?本節(jié)我們學(xué)習(xí)Stream流水線的原理档泽,這是Stream實現(xiàn)的關(guān)鍵所在。
首先回顧一下容器執(zhí)行Lambda表達式的方式揖赴,以ArrayList.forEach()方法為例馆匿,具體代碼如下:
// ArrayList.forEach()
public void forEach(Consumer<? super E> action) {
...
for (int i=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āng)于一個回調(diào)方法铭拧,這很好理解赃蛛。
Stream API中大量使用Lambda表達式作為回調(diào)方法恃锉,但這并不是關(guān)鍵。理解Stream我們更關(guān)心的另外兩個問題:流水線和自動并行呕臂。使用Stream或許很容易寫入如下形式的代碼:
List<String> stringStream = Arrays.asList("Apple","Bug","ABc", "Dog");
System.out.println(stringStream.stream().filter(s -> s.startsWith("A"))
.mapToInt(String::length).max().getAsInt());
上述代碼求出以字母A開頭的字符串的最大長度破托,一種直白的方式是為每一次函數(shù)調(diào)用都執(zhí)一次迭代,這樣做能夠?qū)崿F(xiàn)功能歧蒋,但效率上肯定是無法接受的土砂。類庫的實現(xiàn)著使用流水線(Pipeline)的方式巧妙的避免了多次迭代,其基本思想是在一次迭代中盡可能多的執(zhí)行用戶指定的操作疏尿。為講解方便我們匯總了Stream的所有操作瘟芝。
Stream上的所有操作分為兩類:中間操作和結(jié)束操作,中間操作只是一種標記褥琐,只有結(jié)束操作才會觸發(fā)實際計算。中間操作又可以分為無狀態(tài)的(Stateless)和有狀態(tài)的(Stateful)晤郑,無狀態(tài)中間操作是指元素的處理不受前面元素的影響敌呈,而有狀態(tài)的中間操作必須等到所有元素處理之后才知道最終結(jié)果,比如排序是有狀態(tài)操作造寝,在讀取所有元素之前并不能確定排序結(jié)果磕洪;結(jié)束操作又可以分為短路操作和非短路操作,短路操作是指不用處理全部元素就可以返回結(jié)果诫龙,比如找到第一個滿足條件的元素析显。之所以要進行如此精細的劃分,是因為底層對每一種情況的處理方式不同签赃。
一種直白的實現(xiàn)方式
仍然考慮上述求最長字符串的程序谷异,一種直白的流水線實現(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í)行流程如如所示:
這樣做實現(xiàn)起來非常簡單直觀,但有兩個明顯的弊端:
- 迭代次數(shù)多芽淡。迭代次數(shù)跟函數(shù)調(diào)用的次數(shù)相等马绝。
- 頻繁產(chǎn)生中間結(jié)果。每次函數(shù)調(diào)用都產(chǎn)生一次中間結(jié)果吐绵,存儲開銷無法接受迹淌。
這些弊端使得效率底下河绽,根本無法接受。如果不使用Stream API我們都知道上述代碼該如何在一次迭代中完成唉窃,大致是如下形式:
int longest = 0;
for(String str : strings){
if(str.startsWith("A")){// 1. filter(), 保留以A開頭的字符串
int len = str.length();// 2. mapToInt(), 轉(zhuǎn)換成長度
longest = Math.max(len, longest);// 3. max(), 保留最長的長度
}
}
采用這種方式我們不但減少了迭代次數(shù)耙饰,也避免了存儲中間結(jié)果,顯然這就是流水線纹份,因為我們把三個操作放在了一次迭代當(dāng)中苟跪。只要我們事先知道用戶意圖,總是能夠采用上述方式實現(xiàn)跟Stream API等價的功能蔓涧,但問題是Stream類庫的設(shè)計者并不知道用戶的意圖是什么件已。如何在無法假設(shè)用戶行為的前提下實現(xiàn)流水線,是類庫的設(shè)計者要考慮的問題元暴。
Stream流水線解決方案
我們大致能夠想到篷扩,應(yīng)該采用某種方式記錄用戶每一步的操作,當(dāng)用戶調(diào)用結(jié)束操作時將之前記錄的操作疊加到一起在一次迭代中全部執(zhí)行掉茉盏。沿著這個思路鉴未,有幾個問題需要解決:
- 用戶的操作如何記錄?
- 操作如何疊加鸠姨?
- 疊加之后的操作如何執(zhí)行铜秆?
- 執(zhí)行后的結(jié)果(如果有)在哪里?
操作如何記錄讶迁?
注意這里使用的是“操作(operation)”一詞连茧,指的是“Stream中間操作”的操作,很多Stream操作會需要一個回調(diào)函數(shù)(Lambda表達式)巍糯,因此一個完整的操作是<數(shù)據(jù)來源啸驯,操作,回調(diào)函數(shù)>構(gòu)成的三元組鳞贷。Stream中使用Stage的概念來描述一個完整的操作坯汤,并用某種實例化后的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記錄操作的方式。這就是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做好準備碍沐。 |
void end() | 所有元素遍歷完成之后調(diào)用狸捅,通知Sink沒有更多的元素了。 |
boolean cancellationRequested() | 是否可以結(jié)束操作累提,可以讓短路操作盡早結(jié)束尘喝。 |
void accept(T t) | 遍歷元素時調(diào)用,接受一個待處理元素斋陪,并對元素進行處理朽褪。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()方法也是必須實現(xiàn)的。比如Stream.sorted()是一個有狀態(tài)的中間操作,其對應(yīng)的Sink.begin()方法可能創(chuàng)建一個乘放結(jié)果的容器慰安,而accept()方法負責(zé)將元素添加到該容器攻晒,最后end()負責(zé)對容器進行排序。對于短路操作胰蝠,Sink.cancellationRequested()也是必須實現(xiàn)的,比如Stream.findFirst()是短路操作,只要找到一個元素行贪,cancellationRequested()就應(yīng)該返回true,以便調(diào)用者盡快結(jié)束查找模闲。Sink的四個接口方法常常相互協(xié)作建瘫,共同完成計算任務(wù)。實際上Stream API內(nèi)部實現(xiàn)的的本質(zhì)尸折,就是如何重載Sink的這四個接口方法啰脚。
有了Sink對操作的包裝,Stage之間的調(diào)用問題就解決了实夹,執(zhí)行時只需要從流水線的head開始對數(shù)據(jù)源依次調(diào)用每個Stage對應(yīng)的Sink.{begin(), accept(), cancellationRequested(), end()}方法就可以了橄浓。一種可能的Sink.accept()方法流程是這樣的:
void accept(U u){
1. 使用當(dāng)前Sink包裝的回調(diào)函數(shù)處理u
2. 將處理結(jié)果傳遞給流水線下游的Sink
}
Sink接口的其他幾個方法也是按照這種[處理->轉(zhuǎn)發(fā)]的模型實現(xiàn)。下面我們結(jié)合具體例子看看Stream的中間操作是如何將自身的操作包裝成Sink以及Sink是如何將處理結(jié)果轉(zhuǎn)發(fā)給下一個Sink的亮航。先看Stream.map()方法:
// Stream.map()荸实,調(diào)用該方法將產(chǎn)生一個新的Stream
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
...
return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
@Override /*opWripSink()方法返回由回調(diào)函數(shù)包裝而成Sink*/
Sink<P_OUT> opWrapSink(int flags, Sink<R> downstream) {
return new Sink.ChainedReference<P_OUT, R>(downstream) {
@Override
public void accept(P_OUT u) {
R r = mapper.apply(u);// 1. 使用當(dāng)前Sink包裝的回調(diào)函數(shù)mapper處理u
downstream.accept(r);// 2. 將處理結(jié)果傳遞給流水線下游的Sink
}
};
}
};
}
上述代碼看似復(fù)雜,其實邏輯很簡單缴淋,就是將回調(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ù)雜一點的例子。Stream.sorted()方法將對Stream中的元素進行排序钟沛,顯然這是一個有狀態(tài)的中間操作畔规,因為讀取所有元素之前是沒法得到最終順序的。拋開模板代碼直接進入問題本質(zhì)恨统,sorted()方法是如何將操作封裝成Sink的呢叁扫?sorted()一種可能封裝的Sink代碼如下:
// Stream.sort()方法用到的Sink實現(xiàn)
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
private ArrayList<T> list;// 存放用于排序的元素
RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
super(downstream, comparator);
}
@Override
public void begin(long size) {
...
// 創(chuàng)建一個存放排序元素的列表
list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
}
@Override
public void end() {
list.sort(comparator);// 只有元素全部接收之后才能開始排序
downstream.begin(list.size());
if (!cancellationWasRequested) {// 下游Sink不包含短路操作
list.forEach(downstream::accept);// 2. 將處理結(jié)果傳遞給流水線下游的Sink
}
else {// 下游Sink包含短路操作
for (T t : list) {// 每次都調(diào)用cancellationRequested()詢問是否可以結(jié)束處理。
if (downstream.cancellationRequested()) break;
downstream.accept(t);// 2. 將處理結(jié)果傳遞給流水線下游的Sink
}
}
downstream.end();
list = null;
}
@Override
public void accept(T t) {
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(因為沒有下游)代芜。對于Sink的[處理->轉(zhuǎn)發(fā)]模型,結(jié)束操作的Sink就是調(diào)用鏈的出口浓利。
我們再來考察一下上游的Sink是如何找到下游Sink的蜒犯。一種可選的方案是在PipelineHelper中設(shè)置一個Sink字段,在流水線中找到下游Stage并訪問Sink字段即可荞膘。但Stream類庫的設(shè)計者沒有這么做,而是設(shè)置了一個Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)方法來得到Sink玉工,該方法的作用是返回一個新的包含了當(dāng)前Stage代表的操作以及能夠?qū)⒔Y(jié)果傳遞給downstream的Sink對象羽资。為什么要產(chǎn)生一個新對象而不是返回一個Sink字段?這是因為使用opWrapSink()可以將當(dāng)前操作與下游Sink(上文中的downstream參數(shù))結(jié)合成新Sink遵班。試想只要從流水線的最后一個Stage開始屠升,不斷調(diào)用上一個Stage的opWrapSink()方法直到最開始(不包括stage0,因為stage0代表數(shù)據(jù)源狭郑,不包含操作)腹暖,就可以得到一個代表了流水線上所有操作的Sink,用代碼表示就是這樣:
// AbstractPipeline.wrapSink()
// 從下游向上游不斷包裝Sink翰萨。如果最初傳入的sink代表結(jié)束操作脏答,
// 函數(shù)返回時就可以得到一個代表了流水線上所有操作的Sink。
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
...
for (AbstractPipeline p=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> void copyInto(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ù)進行迭代(Spliterator是容器的一種迭代器黄绩,參閱)羡洁,最后調(diào)用wrappedSink.end()方法通知Sink數(shù)據(jù)處理結(jié)束。邏輯如此清晰爽丹。
執(zhí)行后的結(jié)果在哪里筑煮?
最后一個問題是流水線上所有操作都執(zhí)行后,用戶所需要的結(jié)果(如果有)在哪里粤蝎?首先要說明的是不是所有的Stream結(jié)束操作都需要返回結(jié)果真仲,有些操作只是為了使用其副作用(Side-effects),比如使用Stream.forEach()方法將結(jié)果打印出來就是常見的使用副作用的場景(事實上诽里,除了打印之外其他場景都應(yīng)避免使用副作用)袒餐,對于真正需要返回結(jié)果的結(jié)束操作結(jié)果存在哪里呢?
特別說明:副作用不應(yīng)該被濫用谤狡,也許你會覺得在Stream.forEach()里進行元素收集是個不錯的選擇灸眼,就像下面代碼中那樣,但遺憾的是這樣使用的正確性和效率都無法保證墓懂,因為Stream可能會并行執(zhí)行焰宣。大多數(shù)使用副作用的地方都可以使用歸約操作更安全和有效的完成。
// 錯誤的收集方式
ArrayList<String> results = new ArrayList<>();
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é)束操作 |
---|---|
boolean | anyMatch() allMatch() noneMatch() |
Optional | findFirst() findAny() |
歸約結(jié)果 | reduce() collect() |
數(shù)組 | toArray |
- 對于返回boolean或者Optional的操作(Optional是存放一個值的容器)的操作闪唆,由于值返回一個值,只需要在對應(yīng)的Sink中記錄這個值钓葫,等到執(zhí)行結(jié)束時返回就可以了悄蕾。
- 對于規(guī)約操作,最終結(jié)果放在用戶調(diào)用時指定的容器中(容器類型通過收集器指定)础浮。collect(), reduce(), max(), min()都是歸約操作帆调,雖然max()和min()也是返回一個Optional,但事實上底層是通過調(diào)用reduce()方法實現(xiàn)的豆同。
- 對于返回是數(shù)組的情況番刊,毫無疑問的結(jié)果會放在數(shù)組當(dāng)中。這么說當(dāng)然是對的影锈,但在最終返回數(shù)組之前芹务,結(jié)果其實是存儲在一種叫做Node的數(shù)據(jù)結(jié)構(gòu)中的蝉绷。Node是一種多叉樹結(jié)構(gòu),元素存儲在樹的葉子當(dāng)中锄禽,并且一個葉子節(jié)點可以存放多哥元素潜必。這樣做是為了并行執(zhí)行方便。
本文詳細介紹了Stream流水線的組織方式和執(zhí)行過程沃但,學(xué)習(xí)本文將有助于理解原理并寫出正確的Stream代碼磁滚,同時打消你對Stream API效率方面的顧慮。如你所見宵晚,Stream API實現(xiàn)如此巧妙垂攘,即使我們使用外部迭代手動編寫等價代碼,也未必更加高效淤刃。
原文鏈接:https://objcoding.com/2019/03/04/lambda/
https://github.com/CarpenterLee/JavaLambdaInternals