????Jav8中,在核心類庫中引入了新的概念沫屡,流(Stream)饵隙。流使得程序媛們得以站在更高的抽象層次上對集合進(jìn)行操作。
????今天沮脖,居士將主要介紹Steam類中對應(yīng)集合上操作的幾個重要的方法金矛。
1、 Steam舉例
????對使用Java的程序媛們勺届,當(dāng)需要處理集合里的每一個數(shù)據(jù)時(shí)驶俊,通常是使用迭代,再對每個返回的元素進(jìn)行處理免姿。比如:
int count = 0;
ArrayList<String> nameList = new ArrayList<>();
nameList.add("仁昌居士");
nameList.add("仁昌居士");
nameList.add("痕無羽");
nameList.add("羽無痕");
for (String name: nameList) {
if(name.equals("仁昌居士"))
count++饼酿;
}
????盡管這段代碼思想上 并不難理解,但是存在幾個問題:
????(1) 從代碼量上來看胚膊,每一次的循環(huán)集合類故俐,都需要重復(fù)寫很多的樣板代碼。
????(2)對于for循環(huán)寫的代碼塊紊婉,有些程序媛可能很難理解其編寫意圖药版。需要閱讀整個循環(huán)體后,才能有一定的理解喻犁。假設(shè)只有一個for循環(huán)槽片,相對理解并不難,但是當(dāng)出現(xiàn)多層嵌套循環(huán)肢础,那理解所花費(fèi)的成本就大幅度提升了还栓。
????分析一下for循環(huán)的實(shí)現(xiàn)原理,可知是通過調(diào)用了iterator()方法传轰,產(chǎn)生了一個Iterator對象蝙云,通過while方法遍歷的顯式調(diào)用這個對象的hasNext()和next()方法。以實(shí)現(xiàn)需求路召。這種遍歷過程叫做外部遍歷勃刨,是一種串行化操作波材。
Iterator<String> iterator = nameList.iterator();
while(iterator.hasNext()){
String name = iterator.next();
if(name.equals("仁昌居士")){
count++;
}
}
????注意事項(xiàng):為什么對for循環(huán)叫他外部遍歷而不是外部迭代的原因身隐?可見另一篇文章:還未寫廷区,周末寫。
????相對于外部遍歷贾铝,還有一種方法叫做內(nèi)部遍歷隙轻。通過內(nèi)部遍歷垢揩,將上述代碼實(shí)現(xiàn)為:
long count = nameList.stream().filter(name -> name.equals("仁昌居士"))
.count();
????上述代碼實(shí)際是三步玖绿,第一步:nameList創(chuàng)建了一個Stream實(shí)例叁巨,第二步:用fliter操作符過濾找出為“仁昌居士”的name,并轉(zhuǎn)換成另外一個Stream,第三步:把Stream的里面包含的內(nèi)容按照某種算法來成型成一個值,代碼中式用count操作符計(jì)算有幾個這樣的name显蝌。
2、 惰性求值和及早求值
????通常,在Java中調(diào)有一個方法,計(jì)算機(jī)會隨機(jī)執(zhí)行相應(yīng)的操作,比如通過println在終端上輸出一條信息眼坏。Stream里的方法則有些不同。比如說:
nameList.stream().filter(name -> name.equals("仁昌居士"));
????這行代碼并沒有通過fliter得到新的集合,只是對Stream進(jìn)行了描述诚卸,這種方法叫做“惰性求值”方法棠赛,而之后的“.count()”使Stream產(chǎn)生了值的方法,叫做“及早求值”方法。
????最好的驗(yàn)證方式就如下翘狱。
????單純的在filter中加入一條println語句:
nameList.stream()
.filter(name -> {
System.out.println(name);
return name.equals("仁昌居士");
});
???? 運(yùn)行結(jié)果是程序并沒有輸出對應(yīng)信息赤惊。
????再測試:在后面加入一個及早求值方法勇哗,如count(),將會得到輸出結(jié)果瞧栗。
nameList.stream()
.filter(name -> {
System.out.println(name);
return name.equals("仁昌居士");
})
.count();
????想知道操作符是惰性求值操作符還是及早求值操作符斯稳,只需觀察其返回值海铆,如果返回值是Stream迹恐,則是惰性求值操作符;如果返回值是另一個類型或者是void卧斟,則是及早求值操作符殴边。通過這種多個惰性求值操作符+一個及早求值操作符消費(fèi)為結(jié)尾的鏈來得到想要的值憎茂,這個過程和建造者Builder模式很相似。建造者Builder模式就是通過使用一系列操作設(shè)置屬性和配置锤岸,最后通過一個build方法竖幔,將對象真正創(chuàng)建出來。
3是偷、 常用的Stream操作符
????現(xiàn)在講述幾個比較常用的Stream API拳氢。
3.1 創(chuàng)建Stream操作符
3.1.1 of
????Stream的of操作符,是將一組數(shù)據(jù)生成一個Stream蛋铆。是一個惰性求值操作符馋评。
Stream nameStream = Stream.of("仁昌居士","痕無羽","羽無痕");
3.1.2 generate
????生成一個無限長度的Stream,其元素的生成是通過給定的Supplier(這個接口可以看成一個對象的工廠刺啦,每次調(diào)用返回一個給定類型的對象)留特,也是一個惰性求值操作符。
Stream.generate(() -> Math.random());
????生成一個無限長度的Stream玛瘸,其中值是隨機(jī)的蜕青。這個無限長度Stream是懶加載,一般這種無限長度的Stream都會配合Stream的limit()方法來用糊渊。
3.1.3 iterate
????iterate操作符生成無限長度的Stream右核,和generator不同的是,其元素的生成是重復(fù)對給定的種子值(seed)調(diào)用用戶指定函數(shù)來生成的渺绒。其中包含的元素可以認(rèn)為是:seed蒙兰,f(seed),f(f(seed))無限循環(huán),也是惰性求值操作符
Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
????這段代碼就是先獲取一個無限長度的正整數(shù)集合的Stream芒篷,然后取出前10個打印搜变。千萬注意:使用limit方法,不然會無限打印下去针炉。
3.2 轉(zhuǎn)換Stream操作符
3.2.1 map
????map操作符的作用就是將Stream中的每個值進(jìn)行同一個操作的處理后挠他,再將其轉(zhuǎn)換為一個新的Stream,所以是惰性求值操作符篡帕。
List<String> list = Stream.of(1,2,3)
.map(integer -> String.valueOf(integer))
.collect(Collectors.toList());
????看上面這段代碼可知殖侵,通過map操作符和Lambda表達(dá)式將一個Integer類型的參數(shù)轉(zhuǎn)成了一個String的返回值。參數(shù)和返回值直接不必是同一種類型镰烧,但是Lambda表達(dá)式拢军,必須是Function接口(只包含一個參數(shù)的普通函數(shù)接口)的一個實(shí)例。
????注意事項(xiàng):用map操作符得到的還是Stream怔鳖。
3.2.2 flatMap
????flatMap操作符不同于map操作符將Stream中的值轉(zhuǎn)換為新值茉唉,他能將多個Stream合成一個Stream,返回值也是Stream。是惰性求值操作符度陆。
ArrayList<Integer> arrayList1 = new ArrayList<>();
arrayList1.add(1);
arrayList1.add(2);
ArrayList<Integer> arrayList2 = new ArrayList<>();
arrayList2.add(3);
arrayList2.add(4);
List<Integer> list3 = Stream.of(arrayList1,arrayList2)
.flatMap(numbers -> numbers.stream())
.collect(Collectors.toList());
????通過stream()方法艾凯,將每個ArrayList轉(zhuǎn)換成了Stream對象,其余部分由flatMap操作符處理懂傀,得到的Stream是Stream.of(1趾诗,2,3蹬蚁,4)恃泪。
3.2.3 distinct
????distinct操作符,是對Stream中包含的元素進(jìn)行去重操作(去重邏輯依賴元素的equals方法)犀斋,新生成的Stream中沒有重復(fù)的元素悟泵。是惰性求值操作符。
Stream stream = Stream.of(1, 2, 3, 4,1,2,2,3,4)
.distinct();
????得到的Stream里面的元素只有1,2,3,4四個闪水。重復(fù)的都被去掉了糕非。
3.2.4 filter
????fliter操作符,上文已經(jīng)提及過了球榆,是用于過濾的惰性求值操作符朽肥。
List<Integer> list = Stream.of(1,2,3)
.filter(integer -> integer >1)
.collect(Collectors.toList());
????和map操作符相似,filter操作符接受一個函數(shù)為參數(shù)持钉,該函數(shù)通過Lambda表達(dá)式表示衡招,如這段代碼,Lambda表達(dá)式將會對大于1的返回true每强,否則返回false始腾。這段代碼就是通過filter操作符過濾選擇Lambda表達(dá)式返回值為true的元素保留生成新的Stream,并通過collect操作符得到符合要求的List空执。
3.2.5 peek
????peek生成一個包含原Stream的所有元素的新Stream浪箭,同時(shí)會提供一個消費(fèi)函數(shù)(Consumer實(shí)例),當(dāng)最終用及早求值操作符消費(fèi)此Stream時(shí)辨绊,新Stream每個元素都會執(zhí)行給定的消費(fèi)函數(shù)奶栖。是惰性求值操作符。
nameList.stream()
.filter(name -> name.equals("仁昌居士"))
.peek(name -> System.out.println(name))
.collect(Collectors.toList());
3.2.6 limit
????limit對一個Stream進(jìn)行截?cái)嗖僮髅趴溃@取其前N個元素宣鄙,如果原Stream中包含的元素個數(shù)小于N,那就獲取其所有的元素默蚌,是惰性求值操作符冻晤。
Stream stream = Stream.of(1, 2, 3, 4,5,6,7,8,9,10)
.limit(3);
????得到的新的Stream的元素只有前3個。后面的被截?cái)嗔恕?/p>
3.2.7 skip
????返回一個跳過原Stream的前N個元素后剩下元素組成的新Stream绸吸,如果原Stream中包含的元素個數(shù)小于N鼻弧,那么返回空Stream设江,是惰性求值操作符。
Stream stream = Stream.of(1, 2, 3, 4,5,6,7,8,9,10)
.skip(3);
????得到的新的Stream的元素只有后7個温数。前面的3個被跳過不要了。
3.3 成型(Reduce)Stream操作符
???? 成型(Reduce)Stream操作符和.reduce()操作符是兩個東西蜻势。
????成型(Reduce)是個概念撑刺,我將其理解為將Stream在經(jīng)過多次轉(zhuǎn)換操作后確定最終成型得到一個特定的非Stream的結(jié)果。
????而成型(Reduce)Stream操作符就是對Stream反復(fù)使用某個合并操作握玛,把序列中的元素合并成一個整合結(jié)果的操作符够傍。比如:max操作符、min操作符挠铲、sum操作符冕屯、count操作符、reduce()操作符拂苹、collect操作符等等安聘。
????注意事項(xiàng):其中collect操作符和其他幾個操作符不同。他最終成型的結(jié)果是一個可變的容器瓢棒,比如Collection或者StringBuilder浴韭。
3.3.1 max和min
????Stream中進(jìn)行大小比較是比較常用的操作,所以有了max和min操作符脯宿,返回值類型是Optional念颈,這是Java8防止出現(xiàn)NPE的一種可行方法,后面的文章會詳細(xì)介紹连霉,這里就簡單的認(rèn)為是一個容器榴芳,其中可能會包含0個或者1個對象。跺撼。
????查找Stream中的最大或最小元素窟感,就要考慮是用什么作為排序的指標(biāo)。
Integer integer3 = Stream.of(1, 2, 3, 4)
.min((x,y) -> x.compareTo(y))
.get();
????通過比較兩個對象的值的大小歉井,來得到最小值肌括。對于這個指標(biāo),也可以通過Comparator對象酣难。
Integer integer = Stream.of(1, 2, 3, 4)
.min(Comparator.naturalOrder())
.get();
????max和min方法同理谍夭,意思也一目了然,所以不用過多描述憨募,都是及早求值操作符紧索。
3.3.2 sum
????sum操作符不是所有的Stream對象都有的,只有IntStream菜谣、LongStream和DoubleStream是實(shí)例才有珠漂。
int sum = IntStream.of(1, 2, 3, 4,5,6,7,8,9,10)
.sum ();
????sum為55晚缩。求和的及早求值操作符。
3.3.3 count
????count操作符不是求Stream中元素的數(shù)量媳危。
long count= Stream.of(0,1, 2, 3, 4,5,6,7,8,9)
.count();
????count為10荞彼。求元素個數(shù)的及早求值操作符。
3.3.4 reduce
????reduce操作符是及早求值操作符待笑,接受一個元素序列為輸入鸣皂,反復(fù)使用某個合并操作,把序列中的元素合并成一個匯總的結(jié)果暮蹂,其生成的值不是隨意的寞缝,而是根據(jù)指定的計(jì)算模型。像之前的count仰泻、min荆陆、max操作符都是reduce操作。
????reduce方法有三個override的方法集侯。
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);
???? 先來看reduce方法的第一種形式被啼,其方法定義如下:
Optional<T> reduce(BinaryOperator<T> accumulator);
????接受一個BinaryOperator類型的參數(shù),在使用的時(shí)候我們可以用lambda表達(dá)式來棠枉。
Stream.of(1,2,3,4,5,6,7,8,9,10).reduce((sum, item) -> sum + item).get();
????結(jié)果都為55趟据。
???? 可以看到reduce方法接受一個函數(shù),這個函數(shù)有兩個參數(shù)术健,第一個參數(shù)是上次函數(shù)執(zhí)行的返回值(也稱為中間結(jié)果)汹碱,第二個參數(shù)是stream中的元素,這個函數(shù)把這兩個值相加荞估,得到的和會被賦值給下次執(zhí)行這個函數(shù)的第一個參數(shù)咳促。要注意的是:第一次執(zhí)行的時(shí)候第一個參數(shù)的值是Stream的第一個元素,第二個參數(shù)是Stream的第二個元素勘伺。這個方法返回值類型是Optional跪腹。
????再來看reduce方法的第二種形式,其方法定義如下:
T reduce(T identity, BinaryOperator<T> accumulator);
????與第一種變形相同的是都會接受一個BinaryOperator函數(shù)接口飞醉,不同的是其會接受一個identity參數(shù)冲茸,用來指定Stream循環(huán)的初始值。如果Stream為空缅帘,就直接返回該值轴术。另一方面,該方法不會返回Optional钦无,因?yàn)樵摲椒ú粫霈F(xiàn)null逗栽。
Stream.of(1,2,3,4,5,6,7,8,9,10).reduce(1, (sum, item) -> sum + item)).get();
????結(jié)果都為56。
????變形1失暂,未定義初始值彼宠,從而第一次執(zhí)行的時(shí)候第一個參數(shù)的值是Stream的第一個元素鳄虱,第二個參數(shù)是Stream的第二個元素。
???? 變形2凭峡,定義了初始值拙已,從而第一次執(zhí)行的時(shí)候第一個參數(shù)的值是初始值,第二個參數(shù)是Stream的第一個元素摧冀。
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
????對于第三種變形倍踪,我們先看各個參數(shù)的含義,第一個參數(shù)類型是實(shí)際返回實(shí)例的數(shù)據(jù)類型按价,同時(shí)其為一個泛型也就是意味著該變形的可以返回任意類型的數(shù)據(jù)惭适,第二個參數(shù)累加器accumulator笙瑟,可以使用二元?表達(dá)式(即二元lambda表達(dá)式)楼镐,聲明你在u上累加你的數(shù)據(jù)來源t的邏輯,例如(u,t)->u.sum(t),此時(shí)lambda表達(dá)式的行參列表是返回實(shí)例u和遍歷的集合元素t往枷,函數(shù)體是在u上累加t框产,第三個參數(shù)組合器combiner,同樣是二元?表達(dá)式错洁,(u,t)->u秉宿, 是用來處理并發(fā)操作的。因?yàn)镾tream是支持并發(fā)操作的屯碴,為了避免競爭描睦,對于reduce線程都會有獨(dú)立的result,combiner的作用在于合并每個線程的result得到最終結(jié)果导而。這也說明了了第三個函數(shù)參數(shù)的數(shù)據(jù)類型必須為返回?cái)?shù)據(jù)類型了忱叭。代碼并不好舉例,先不距離今艺,在以后的講解中會提及韵丑。
3.3.5 collect
???? collect操作符:是一個及早求值操作符。它可以把Stream中的要有元素收集到一個結(jié)果容器中(比如Collection)虚缎。先看一下最通用的collect方法的定義(還有其他override方法)撵彻。
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
????先來看看這三個參數(shù)的含義:Supplier supplier是一個工廠函數(shù),用來生成一個新的容器实牡;BiConsumer accumulator也是一個函數(shù)陌僵,用來把Stream中的元素添加到結(jié)果容器中;BiConsumer combiner還是一個函數(shù)创坞,用來把中間狀態(tài)的多個結(jié)果容器合并成為一個(并發(fā)的時(shí)候會用到)拾弃。
List<Integer> numsWithoutNull = Stream.of(1,2,3,4,5,6,7,8,9,10)
.collect(() -> new ArrayList<Integer>(),(list, item) -> list.add(item),(list1, list2) -> list1.addAll(list2));
????上面這段代碼就是把一個元素是Integer類型的List收集到一個新的List中。進(jìn)一步看一下collect方法的三個參數(shù)摆霉,都是lambda形式的函數(shù)豪椿。
???? 第一個函數(shù)生成一個新的ArrayList實(shí)例奔坟;
????第二個函數(shù)接受兩個參數(shù),第一個是前面生成的ArrayList對象搭盾,二個是stream中包含的元素咳秉,函數(shù)體就是把stream中的元素加入ArrayList對象中。第二個函數(shù)被反復(fù)調(diào)用直到原stream的元素被消費(fèi)完畢鸯隅;
????第三個函數(shù)也是接受兩個參數(shù)澜建,這兩個都是ArrayList類型的,函數(shù)體就是把多個ArrayList容器合并成為一個。
???? 但是上面的collect方法調(diào)用有些復(fù)雜了,有更簡單的override方法揩环,其依賴Collector谐算。
<R, A> R collect(Collector<? super T, A, R> collector);
????進(jìn)一步,Java8還給我們提供了Collector的工具類–Collectors距误,其中已經(jīng)定義了一些靜態(tài)工廠方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中,等等奸攻。這樣的靜態(tài)方法還有很多,這里就不一一介紹了虱痕,大家可以直接去看文檔睹耐。下面看看使用Collectors對于代碼的簡化:
List<Integer> numsWithoutNull = Stream.of(1,2,3,4,5,6,7,8,9,10)
.collect(Collectors.toList());
????這段代碼將of()操作符得到的Stream,用collect(Collectors.toList())操作符從Stream中生成一個List部翘。
4硝训、 性能問題
????完成了上述的講解,會發(fā)現(xiàn)在使用操作符時(shí)新思,會出現(xiàn)對于一個Stream進(jìn)行多次轉(zhuǎn)換操作窖梁,每次都對Stream的每個元素進(jìn)行轉(zhuǎn)換,而且是執(zhí)行多次表牢,這樣時(shí)間復(fù)雜度就是一個for循環(huán)里把所有操作都做掉的N(轉(zhuǎn)換的次數(shù))倍啊窄绒。其實(shí)不是這樣的,轉(zhuǎn)換操作都是lazy的崔兴,多個轉(zhuǎn)換操作只會在成型(Reduce)操作的時(shí)候融合起來彰导,一次循環(huán)完成。我們可以這樣簡單的理解敲茄,Stream里有個操作函數(shù)的集合位谋,每次轉(zhuǎn)換操作就是把轉(zhuǎn)換函數(shù)放入這個集合中,在成型(Reduce)操作的時(shí)候循環(huán)Stream對應(yīng)的集合堰燎,然后對每個元素一次性執(zhí)行所有的操作掏父。
5、總結(jié)
????對于Stream秆剪,單純的書面理解是很難明白的赊淑,碼字看方法才是最好的學(xué)習(xí)方法爵政。所以我的御姐兒,你還是多碼碼代碼吧陶缺。本居士很忙的啊钾挟。