Java8 Stream,簡潔快速處理集合(上)

Howie_Y發表於2018-08-10

Java 8 釋出至今也已經好幾年過去,如今 Java 也已經向 11 邁去,但是 Java 8 作出的改變可以說是革命性的,影響足夠深遠,學習 Java 8 應該是 Java 開發者的必修課。

今天給大家帶來 Java 8 Stream 講解,為什麼直接講這個,是因為只要你學完,立刻就能上手,並能讓它在你的程式碼中大展身手。

值得注意的是:學習 Stream 之前必須先學習 lambda 的相關知識。本文也假設讀者已經掌握 lambda 的相關知識

本篇文章主要內容:

  • 介紹 Stream 以及 Stream 是如何處理集合的
  • 介紹 Stream 與集合的關係與區別
  • Stream 的基本方法介紹

一. 什麼是 Stream

Stream 中文稱為 “流”,通過將集合轉換為這麼一種叫做 “流” 的元素序列,通過宣告性方式,能夠對集合中的每個元素進行一系列並行或序列的流水線操作

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

二. 流操作

Java8 Stream,簡潔快速處理集合(上)

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

其中資料來源便是原始集合,然後將如 List 的集合轉換為 Stream 型別的流,並對流進行一系列的中間操作,比如過濾保留部分元素、對元素進行排序、型別轉換等;最後再進行一個終端操作,可以把 Stream 轉換回集合型別,也可以直接對其中的各個元素進行處理,比如列印、比如計算總數、計算最大值等等

很重要的一點是,很多流操作本身就會返回一個流,所以多個操作可以直接連線起來,我們來看看一條 Stream 操作的程式碼:

Java8 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);
複製程式碼

相關閱讀

猜你喜歡

你的關注就是我不斷髮文最大的動力

相關文章