Stream,集合操作利器,讓你好用到飛起來
作者:xcbeyond
瘋狂源自夢想,技術成就輝煌!微信公眾號:《程序猿技術大咖》號主,專注后端開發(fā)多年,擁有豐富的研發(fā)經驗,樂于技術輸出、分享,現階段從事微服務架構項目的研發(fā)工作,涉及架構設計、技術選型、業(yè)務研發(fā)等工作。對于Java、微服務、數據庫、Docker有深入了解,并有大量的調優(yōu)經驗。
集合是Java中使用最多的API,幾乎每個程序員天天都會和它打招呼,它可以讓你把相同、相似、有關聯的數據整合在一起,便于使用、提取以及運算等操作。在實際Java程序中,集合的使用往往隨著業(yè)務需求、復雜度而變得更加復雜,在這其中將可能會涉及到更多的運算,如:求和、平均值、分組、過濾、排序等等。如何這些操作混合出現,又該如何實現?難道遍歷、再遍歷、再運算么?拋開性能因素,這些操作已經嚴重影響了代碼的整潔,這種代碼也沒有幾個人愿意來讀。
那么,有沒有什么好的辦法來解決這種現狀呢?畢竟集合最為最常用的操作,難道Java語言的設計者沒有意識到這一點嗎?如何能夠幫助你節(jié)約寶貴的時間,讓程序員活得更輕松一點呢?
你可能已經猜到了,答案就是流—Stream。
本文將從JDK1.8中Stream API講起,讓你覺得集合操作原來可以這么輕松使用。
(在學習本節(jié)之前,必須先學習Lambda表達式相關知識,不清楚的可以翻看前幾篇文章JDK1.8新特性(三):Lambda表達式,讓你愛不釋手、JDK1.8新特性(四):函數式接口)
一、Stream是什么
Stream是Java API中的新成員,它允許你以聲明的方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現),你可以把它看成是遍歷數據集的高級迭代器。此外,Stream還可以透明地并行處理,而無需寫任何多線程代碼了。
我們先簡單的對比使用下Stream的好處吧。下面兩段代碼都是實現篩選出名字中包含“xc”字符串的人,并按照其年齡進行排序。
傳統(tǒng)方式(JDK1.8之前,非Stream流):
List<People> peoples = new ArrayList<>();
// 遍歷 + 判斷
for (People people : allPeoples) {
if (people.getName().contains("xc")) {
peoples.add(people);
}
}
// 對年齡排序
Collections.sort(peoples, new Comparator<People>() {
@Override
public int compare(People p1, People p2) {
return Integer.compare(p1.getAge(), p2.getAge());
}
});
Stream方式:
List<People> peoples2 = allPeoples.stream()
.filter(people -> people.getName().contains("xc"))
.sorted(Comparator.comparing(People::getAge))
.collect(Collectors.toList());
沒有對比就沒有傷害,效果顯而易見。從開發(fā)角度來看,Stream方式有以下顯而易見的好處:
代碼以聲明方式寫的:說明想要完成什么(篩選出滿足條件的數據)而不是說明如何實現一個操作(利用循環(huán)和if條件等控制流語句)。
多個基本操作鏈接起來:將多個基礎操作鏈接起來,來表達復雜的數據處理流水線(如下圖),同時體現了代碼的清晰、可讀性。
Stream API功能非常強大,類似上面Stream處理流水線方式應用場景很多,理論上可以生成一個具有無窮長的流水線的。更重要的是,在復雜業(yè)務中你用不著為了讓某些數據處理任務并行而去操心線程和鎖了,Stream API都替你做好了!
Stream,即:”流“,通過將集合轉換為一種叫做”流“的元素序列,通過聲明方式,對集合中的每個元素進行一系列并行或串行的流水線操作。
換句話說,你只需要告訴流你的要求,流便會在背后自行根據要求對元素進行處理,而你只需要 “坐享其成”。
二、Stream操作
整個流操作就是一條流水線,將元素放在流水線上一個個地進行處理,如下圖所示。
其中,數據源是原始集合數據,然后將如 List<T>的集合轉換為Stream<T>類型的流,并對流進行一系列的操作,比如過濾保留部分元素、對元素進行排序、類型轉換等,最后再進行一個終止操作,可以把 Stream 轉換回集合類型,也可以直接對其中的各個元素進行處理,比如打印、比如計算總數、計算最大值等。
很多流操作本身就會返回一個流,所以多個操作可以直接連接起來,就如上面舉例中Stream方式的代碼一樣。
如果是以前,進行這么一系列操作,你需要做個迭代器或者 foreach 循環(huán),然后遍歷,一步步地親力親為地去完成這些操作。但是如果使用流,你便可以直接聲明式地下指令,流會幫你完成這些操作。
通過上面Stream操作流水線、實例,Stream操作大體上分為兩種:中間操作符和終止操作符。
1. 中間操作符
對于數據流來說,中間操作符在執(zhí)行指定處理邏輯后,數據流依然可以傳遞給下一級的操作符。
中間操作符包含8種:
map(mapToInt,mapToLong,mapToDouble) 轉換操作:把比如A->B,這里默認提供了轉int,long,double的操作符。
flatmap(flatmapToInt,flatmapToLong,flatmapToDouble)拍平操作:比如把int[]{2,3,4}拍平變成 2,3,4,也就是從原來的一個數據變成了3個數據,這里默認提供了拍平成int,long,double的操作。
limit限流操作:比如數據流中有10個,我只要前3個就可以使用。
distinct去重操作:重復元素去重。
filter過濾操作:對集合數據進行過濾。
peek消費操作:如果想對數據進行某些操作,如:讀取、編輯修改等。
skip跳過操作:跳過某些元素。
sorted排序操作:對元素排序,前提是實現Comparable接口,當然也可以自定義比較器。
(具體可參照源碼java.util.stream.Stream)
2. 終止操作符
數據經過一系列的中間操作,就輪到終止操作符上場了。終止操作符就是用來對數據進行收集或者消費的,數據到了終止操作這里就不會向下流動了,終止操作符只能使用一次。
collect收集操作:將所有數據收集起來,這個操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以說Stream的核心在于Collectors。
count統(tǒng)計操作:統(tǒng)計最終的數據個數。
findFirst、findAny查找操作:查找第一個、查找任何一個,返回的類型為Optional。
noneMatch、allMatch、anyMatch匹配操作:數據流中是否存在符合條件的元素,返回值為bool 值。
min、max最值操作:需要自定義比較器,返回數據流中最大、最小的值。
reduce規(guī)約操作:將整個數據流的值規(guī)約為一個值,count、min、max底層就是使用reduce。
forEach、forEachOrdered遍歷操作:這里就是對最終的數據進行消費了。
toArray數組操作:將數據流的元素轉換成數組。
說了這么多,心動不如行動,俗話說:實踐出真理。那么,一起來實戰(zhàn)吧。
三、實戰(zhàn)演練
Stream的一系列操作,必須要使用終止操作符,否則整個數據流是不會執(zhí)行起來的。
1. map
轉換、映射操作,將元素轉換成其他形式或提取一些信息。
比如,從People集合中獲取所有人的年齡:
allPeoples.stream()
.map(People::getAge)
.forEach(System.out::println);
2. flatmap
將元素拍平拍扁 ,將拍扁的元素重新組成Stream,并將這些Stream 串行合并成一條Stream。
比如,帶有-字符的字符串進行拆分,并輸出:
Stream.of("x-c-b-e-y-o-n-d","a-b-c-d")
.flatMap(m -> Stream.of(m.split("-")))
.forEach(System.out::println);
輸出:
x
c
b
e
y
o
n
d
a
b
c
d
3. limit
限流操作,限制集合元素的個數。
比如,集合中有10個元素,我只要前4個就可以使用:
Stream.of(1,2,3,4,5,6,7,8,9,10)
.limit(4)
.forEach(System.out::println);
輸出:
1
2
3
4
4. distinct
去重操作,重復元素去重,類似數據庫中的關鍵字distinct。
比如,集合中可能存在一些重復的數據,需要進行去重操作:
Stream.of("xcbeyond","Niki","Liky","xcbeyond")
.distinct()
.forEach(System.out::println);
輸出:
xcbeyond
Niki
Liky
5. filter
過濾、篩選,對某些元素進行過濾,不符合篩選條件的將無法進入流的下游。
比如,篩選出一個數字集合中的所有偶數:
Stream.of(1,2,3,4,5,6,7,8,9,10)
.filter(n -> 0 == n%2)
.forEach(System.out::println);
輸出:
2
4
6
8
10
6. peek
消費操作,如果想對數據進行某些操作,如:讀取、編輯修改等。
比如,將People集合中name統(tǒng)一修改為name+age的形式:
allPeoples.stream()
.peek(people -> people.setName(people.getName() + people.getAge()))
.forEach(people -> System.out.println(people.getName()));
7. skip
跳過操作,跳過某些元素。
比如,一個數字集合,跳過前4個元素:
Stream.of(1,2,3,4,5,6,7,8,9,10)
.skip(4)
.forEach(System.out::println);
輸出:
5
6
7
8
9
10
8. sorted
排序操作,對元素排序,前提是實現Comparable接口,當然也可以自定義比較器。
比如,將People集合按照年齡排序:
allPeoples.stream()
.sorted(Comparator.comparing(People::getAge))
.forEach(System.out::println);
9. collect
收集操作,終止操作符,用于將最終的數據收集到新的集合中,如,List、Set、Map等集合。
比如,將People集合按照年齡排序,并存放在一個新的集合中,供后續(xù)使用:
List<People> sortedPeople = allPeoples.stream()
.sorted(Comparator.comparing(People::getAge))
.collect(Collectors.toList());
10. count
統(tǒng)計操作,用于對集合元素個數的統(tǒng)計,返回類型是long。
比如,計算People集合中年齡大于30的人數:
long count = allPeoples.stream()
.filter(people -> people.getAge() > 30)
.count();
11. findFirst、findAny
查找操作,查找第一個、任何一個,返回的類型為Optional。常用于查詢集中符合條件的元素,并結合Optional.isPresent()進行判斷,防止出現未找到而強制獲取數據元素的異常情況。
比如,查找People集合中名字為xcbeyond的人:
People xcbeyondPeople = null;
Optional<People> optional = allPeoples.stream()
.filter(people -> "xcbeyond".equals(people.getName()))
.findFirst();
if (optional.isPresent()) {
xcbeyondPeople = optional.get();
}
12. noneMatch、allMatch、anyMatch
匹配操作,判斷數據流中是否存在符合條件的元素,返回值為boolean值。
noneMatch:沒有匹配條件的元素
allMatch、anyMatch:全匹配
比如,判斷People集合中是否有名字為xcbeyond的人:
boolean bool = allPeoples.stream()
.allMatch(people -> "xcbeyond".equals(people.getName()));
13. min、max
最值操作,根據自定義比較器,返回數據流中最大、最小的元素。
比如,找到People集合中最大年齡的人:
People maxAgePeople = null;
Optional<People> maxAgeOptional = allPeoples.stream()
.max(Comparator.comparing(People::getAge));
if (maxAgeOptional.isPresent()) { // 可能沒有,則需要進行判斷
maxAgePeople = maxAgeOptional.get();
}
14. reduce
規(guī)約操作:將整個數據流的值規(guī)約為一個值,其中count、min、max底層就是使用reduce。
比如,對一個整數集合進行求和:
int sum = Stream.of(1,9,8,4,5,6,-1)
.reduce(0,(e1,e2)->e1+e2);
System.out.println(sum);
輸出:
32
四、總結
本文就Stream的基礎使用層面進行了全面的介紹、實戰(zhàn),告訴你該怎么用每種操作符,只有掌握了這些基本的操作,在面對實際復雜處理邏輯時,需要進一步配合使用,就會知道它的妙處了。這也讓你對集合的操作更上一步,為你省去了不少麻煩。關于Stream更深入的說明,如:并行處理、是否高效等,將會在之后的章節(jié)進行詳盡的闡述、驗證,以消除使用中的疑惑與擔憂。