JDK1.8新特性(五):Stream,集合操作利器,讓你好用到飛起來

在這裏插入圖片描述
前期回顧:
JDK1.8新特性(一):JDK1.8究竟有哪些新特性呢
JDK1.8新特性(二):爲什麼要關注JDK1.8
JDK1.8新特性(三):Lambda表達式,讓你愛不釋手
JDK1.8新特性(四):函數式接口

集合是Java中使用最多的API,幾乎每個程序員天天都會和它打招呼,它可以讓你把相同、相似、有關聯的數據整合在一起,便於使用、提取以及運算等操作。在實際Java程序中,集合的使用往往隨着業務需求、複雜度而變得更加複雜,在這其中將可能會涉及到更多的運算,如:求和、平均值、分組、過濾、排序等等。如何這些操作混合出現,又該如何實現?難道遍歷、再遍歷、再運算麼?拋開性能因素,這些操作已經嚴重影響了代碼的整潔,這種代碼也沒有幾個人願意來讀。

那麼,有沒有什麼好的辦法來解決這種現狀呢?畢竟集合最爲最常用的操作,難道Java語言的設計者沒有意識到這一點嗎?如何能夠幫助你節約寶貴的時間,讓程序員活得更輕鬆一點呢?

你可能已經猜到了,答案就是流—Stream

本文將從JDK1.8Stream API講起,讓你覺得集合操作原來可以這麼輕鬆使用。

(在學習本節之前,必須先學習Lambda表達式相關知識,不清楚的可以翻看前幾篇文章JDK1.8新特性(三):Lambda表達式,讓你愛不釋手JDK1.8新特性(四):函數式接口

一、Stream是什麼

StreamJava API中的新成員,它允許你以聲明的方式處理數據集合(通過查詢語句來表達,而不是臨時編寫一個實現),你可以把它看成是遍歷數據集的高級迭代器。此外,Stream還可以透明地並行處理,而無需寫任何多線程代碼了。

我們先簡單的對比使用下Stream的好處吧。下面兩段代碼都是實現篩選出名字中包含“xc”字符串的人,並按照其年齡進行排序。

傳統方式(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());

沒有對比就沒有傷害,效果顯而易見。從開發角度來看,Stream方式有以下顯而易見的好處:

  • 代碼以聲明方式寫的:說明想要完成什麼(篩選出滿足條件的數據)而不是說明如何實現一個操作(利用循環和if條件等控制流語句)。

  • 多個基本操作鏈接起來:將多個基礎操作鏈接起來,來表達複雜的數據處理流水線(如下圖),同時體現了代碼的清晰、可讀性。

在這裏插入圖片描述

Stream API功能非常強大,類似上面Stream處理流水線方式應用場景很多,理論上可以生成一個具有無窮長的流水線的。更重要的是,在複雜業務中你用不着爲了讓某些數據處理任務並行而去操心線程和鎖了,Stream API都替你做好了!

Stream,即:”流“,通過將集合轉換爲一種叫做”流“的元素序列,通過聲明方式,對集合中的每個元素進行一系列並行或串行的流水線操作。

換句話說,你只需要告訴流你的要求,流便會在背後自行根據要求對元素進行處理,而你只需要 “坐享其成”

二、Stream操作

整個流操作就是一條流水線,將元素放在流水線上一個個地進行處理,如下圖所示。

在這裏插入圖片描述

其中,數據源是原始集合數據,然後將如 List<T>的集合轉換爲Stream<T>類型的流,並對流進行一系列的操作,比如過濾保留部分元素、對元素進行排序、類型轉換等,最後再進行一個終止操作,可以把 Stream 轉換回集合類型,也可以直接對其中的各個元素進行處理,比如打印、比如計算總數、計算最大值等。

很多流操作本身就會返回一個流,所以多個操作可以直接連接起來,就如上面舉例中Stream方式的代碼一樣。
在這裏插入圖片描述

如果是以前,進行這麼一系列操作,你需要做個迭代器或者 foreach 循環,然後遍歷,一步步地親力親爲地去完成這些操作。但是如果使用流,你便可以直接聲明式地下指令,流會幫你完成這些操作。

通過上面Stream操作流水線、實例,Stream操作大體上分爲兩種:中間操作符和終止操作符。

1. 中間操作符

對於數據流來說,中間操作符在執行指定處理邏輯後,數據流依然可以傳遞給下一級的操作符。

中間操作符包含8種:

  • map(mapToInt,mapToLong,mapToDouble) 轉換操作:把比如A->B,這裏默認提供了轉intlongdouble的操作符。

  • 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統計操作:統計最終的數據個數。

  • findFirst、findAny查找操作:查找第一個、查找任何一個,返回的類型爲Optional。

  • noneMatch、allMatch、anyMatch匹配操作:數據流中是否存在符合條件的元素,返回值爲bool 值。

  • min、max最值操作:需要自定義比較器,返回數據流中最大、最小的值。

  • reduce規約操作:將整個數據流的值規約爲一個值,count、min、max底層就是使用reduce。

  • forEach、forEachOrdered遍歷操作:這裏就是對最終的數據進行消費了。

  • toArray數組操作:將數據流的元素轉換成數組。

說了這麼多,心動不如行動,俗話說:實踐出真理。那麼,一起來實戰吧。

三、實戰演練

Stream的一系列操作,必須要使用終止操作符,否則整個數據流是不會執行起來的。

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統一修改爲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集合按照年齡排序,並存放在一個新的集合中,供後續使用:

List<People> sortedPeople = allPeoples.stream()
	.sorted(Comparator.comparing(People::getAge))
	.collect(Collectors.toList());

10. count

統計操作,用於對集合元素個數的統計,返回類型是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

規約操作:將整個數據流的值規約爲一個值,其中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的基礎使用層面進行了全面的介紹、實戰,告訴你該怎麼用每種操作符,只有掌握了這些基本的操作,在面對實際複雜處理邏輯時,需要進一步配合使用,就會知道它的妙處了。這也讓你對集合的操作更上一步,爲你省去了不少麻煩。關於Stream更深入的說明,如:並行處理、是否高效等,將會在之後的章節進行詳盡的闡述、驗證,以消除使用中的疑惑與擔憂。

下一篇,將會提到Stream的終極操作——Collectors,讓集合也能像數據庫SQL一樣,來完成複雜的分組、連接、彙總等,真正讓你好用到飛起來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章