Java 8 釋出至今也已經好幾年過去,如今 Java 也已經向 11 邁去,但是 Java 8 作出的改變可以說是革命性的,影響足夠深遠,學習 Java 8 應該是 Java 開發者的必修課。
今天給大家帶來 Java 8 Stream 講解,為什麼直接講這個,是因為只要你學完,立刻就能上手,並能讓它在你的程式碼中大展身手。
值得注意的是:學習 Stream 之前必須先學習 lambda 的相關知識。本文也假設讀者已經掌握 lambda 的相關知識。
本篇文章主要內容:
- 介紹 Stream 以及 Stream 是如何處理集合的
- 介紹 Stream 與集合的關係與區別
- Stream 的基本方法介紹
一. 什麼是 Stream
Stream 中文稱為 “流”,通過將集合轉換為這麼一種叫做 “流” 的元素序列,通過宣告性方式,能夠對集合中的每個元素進行一系列並行或序列的流水線操作。
換句話說,你只需要告訴流你的要求,流便會在背後自行根據要求對元素進行處理,而你只需要 “坐享其成”。
二. 流操作
整個流操作就是一條流水線,將元素放在流水線上一個個地進行處理。
其中資料來源便是原始集合,然後將如 List 的集合轉換為 Stream 型別的流,並對流進行一系列的中間操作,比如過濾保留部分元素、對元素進行排序、型別轉換等;最後再進行一個終端操作,可以把 Stream 轉換回集合型別,也可以直接對其中的各個元素進行處理,比如列印、比如計算總數、計算最大值等等
很重要的一點是,很多流操作本身就會返回一個流,所以多個操作可以直接連線起來,我們來看看一條 Stream 操作的程式碼:
如果是以前,進行這麼一系列操作,你需要做個迭代器或者 foreach 迴圈,然後遍歷,一步步地親力親為地去完成這些操作;但是如果使用流,你便可以直接宣告式地下指令,流會幫你完成這些操作。
有沒有想到什麼類似的?是的,就像 SQL 語句一樣, select username from user where id = 1
,你只要說明:“我需要 id 是 1 (id = 1)的使用者(user)的使用者名稱(username )”,那麼就可以得到自己想要的資料,而不需要自己親自去資料庫裡面迴圈遍歷查詢。
三. 流與集合
什麼時候計算
Stream 和集合的其中一個差異在於什麼時候進行計算。
一個集合,它會包含當前資料結構中所有的值,你可以隨時增刪,但是集合裡面的元素毫無疑問地都是已經計算好了的。
流則是按需計算,按照使用者的需要計算資料,你可以想象我們通過搜尋引擎進行搜尋,搜尋出來的條目並不是全部呈現出來的,而且先顯示最符合的前 10 條或者前 20 條,只有在點選 “下一頁” 的時候,才會再輸出新的 10 條。
再比方線上觀看電影和你硬碟裡面的電影,也是差不多的道理。
外部迭代和內部迭代
Stream 和集合的另一個差異在於迭代。
我們可以把集合比作一個工廠的倉庫,一開始工廠比較落後,要對貨物作什麼修改,只能工人親自走進倉庫對貨物進行處理,有時候還要將處理後的貨物放到一個新的倉庫裡面。在這個時期,我們需要親自去做迭代,一個個地找到需要的貨物,並進行處理,這叫做外部迭代。
後來工廠發展了起來,配備了流水線作業,只要根據需求設計出相應的流水線,然後工人只要把貨物放到流水線上,就可以等著接收成果了,而且流水線還可以根據要求直接把貨物輸送到相應的倉庫。這就叫做內部迭代,流水線已經幫你把迭代給完成了,你只需要說要幹什麼就可以了(即設計出合理的流水線)。
Java 8 引入 Stream 很大程度是因為,流的內部迭代可以自動選擇一種合適你硬體的資料表示和並行實現;而以往程式設計師自己進行 foreach 之類的時候,則需要自己去管理並行等問題。
一次性的流
流和迭代器類似,只能迭代一次。
Stream<String> stream = list.stream().map(Person::getName).sorted().limit(10);
List<String> newList = stream.collect(toList());
List<String> newList2 = stream.collect(toList());
複製程式碼
上面程式碼中第三行會報錯,因為第二行已經使用過這個流,這個流已經被消費掉了
四. 方法介紹,開始實戰
首先我們先建立一個 Person 泛型的 List
List<Person> list = new ArrayList<>();
list.add(new Person("jack", 20));
list.add(new Person("mike", 25));
list.add(new Person("tom", 30));
複製程式碼
Person 類包含年齡和姓名兩個成員變數
private String name;
private int age;
複製程式碼
1. stream() / parallelStream()
最常用到的方法,將集合轉換為流
List list = new ArrayList();
// return Stream<E>
list.stream();
複製程式碼
而 parallelStream() 是並行流方法,能夠讓資料集執行並行操作,後面會更詳細地講解
2. filter(T -> boolean)
保留 boolean 為 true 的元素
保留年齡為 20 的 person 元素
list = list.stream()
.filter(person -> person.getAge() == 20)
.collect(toList());
列印輸出 [Person{name='jack', age=20}]
複製程式碼
collect(toList()) 可以把流轉換為 List 型別,這個以後會講解
3. distinct()
去除重複元素,這個方法是通過類的 equals 方法來判斷兩個元素是否相等的
如例子中的 Person 類,需要先定義好 equals 方法,不然類似[Person{name='jack', age=20}, Person{name='jack', age=20}]
這樣的情況是不會處理的
4. sorted() / sorted((T, T) -> int)
如果流中的元素的類實現了 Comparable 介面,即有自己的排序規則,那麼可以直接呼叫 sorted() 方法對元素進行排序,如 Stream
反之, 需要呼叫 sorted((T, T) -> int)
實現 Comparator 介面
根據年齡大小來比較:
list = list.stream()
.sorted((p1, p2) -> p1.getAge() - p2.getAge())
.collect(toList());
複製程式碼
當然這個可以簡化為
list = list.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(toList());
複製程式碼
5. limit(long n)
返回前 n 個元素
list = list.stream()
.limit(2)
.collect(toList());
列印輸出 [Person{name='jack', age=20}, Person{name='mike', age=25}]
複製程式碼
6. skip(long n)
去除前 n 個元素
list = list.stream()
.skip(2)
.collect(toList());
列印輸出 [Person{name='tom', age=30}]
複製程式碼
tips:
- 用在 limit(n) 前面時,先去除前 m 個元素再返回剩餘元素的前 n 個元素
- limit(n) 用在 skip(m) 前面時,先返回前 n 個元素再在剩餘的 n 個元素中去除 m 個元素
list = list.stream()
.limit(2)
.skip(1)
.collect(toList());
列印輸出 [Person{name='mike', age=25}]
複製程式碼
7. map(T -> R)
將流中的每一個元素 T 對映為 R(類似型別轉換)
List<String> newlist = list.stream().map(Person::getName).collect(toList());
複製程式碼
newlist 裡面的元素為 list 中每一個 Person 物件的 name 變數
8. flatMap(T -> Stream)
將流中的每一個元素 T 對映為一個流,再把每一個流連線成為一個流
List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");
list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());
複製程式碼
上面例子中,我們的目的是把 List 中每個字串元素以" "分割開,變成一個新的 List。
首先 map 方法分割每個字串元素,但此時流的型別為 Stream<String[ ]>,因為 split 方法返回的是 String[ ] 型別;所以我們需要使用 flatMap 方法,先使用Arrays::stream
將每個 String[ ] 元素變成一個 Stream 流,然後 flatMap 會將每一個流連線成為一個流,最終返回我們需要的 Stream
9. anyMatch(T -> boolean)
流中是否有一個元素匹配給定的 T -> boolean
條件
是否存在一個 person 物件的 age 等於 20:
boolean b = list.stream().anyMatch(person -> person.getAge() == 20);
複製程式碼
10. allMatch(T -> boolean)
流中是否所有元素都匹配給定的 T -> boolean
條件
11. noneMatch(T -> boolean)
流中是否沒有元素匹配給定的 T -> boolean
條件
12. findAny() 和 findFirst()
- findAny():找到其中一個元素 (使用 stream() 時找到的是第一個元素;使用 parallelStream() 並行時找到的是其中一個元素)
- findFirst():找到第一個元素
值得注意的是,這兩個方法返回的是一個 Optional 物件,它是一個容器類,能代表一個值存在或不存在,這個後面會講到
13. reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)
用於組合流中的元素,如求和,求積,求最大值等
計算年齡總和:
int sum = list.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
與之相同:
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
複製程式碼
其中,reduce 第一個引數 0 代表起始值為 0,lambda (a, b) -> a + b
即將兩值相加產生一個新值
同樣地:
計算年齡總乘積:
int sum = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);
複製程式碼
當然也可以
Optional<Integer> sum = list.stream().map(Person::getAge).reduce(Integer::sum);
複製程式碼
即不接受任何起始值,但因為沒有初始值,需要考慮結果可能不存在的情況,因此返回的是 Optional 型別
13. count()
返回流中元素個數,結果為 long 型別
14. collect()
收集方法,我們很常用的是 collect(toList())
,當然還有 collect(toSet())
等,引數是一個收集器介面,這個後面會另外講
15. forEach()
返回結果為 void,很明顯我們可以通過它來幹什麼了,比方說:
### 16. unordered()
還有這個比較不起眼的方法,返回一個等效的無序流,當然如果流本身就是無序的話,那可能就會直接返回其本身
列印各個元素:
list.stream().forEach(System.out::println);
複製程式碼
再比如說 MyBatis 裡面訪問資料庫的 mapper 方法:
向資料庫插入新元素:
list.stream().forEach(PersonMapper::insertPerson);
複製程式碼
相關閱讀
猜你喜歡
- 你必須搞清楚的String,StringBuilder,StringBuffer
- 分享一些 Java 後端的個人乾貨
- 教你 Shiro + SpringBoot 整合 JWT
- 教你 Shiro 整合 SpringBoot,避開各種坑