簡潔又快速地處理集合——Java8 Stream(下)

上一篇文章我講解 Stream 流的基本原理,以及它與集合的區別關係,講了那麼多抽象的,本篇文章我們開始實戰,講解流的各個方法以及各種操作

沒有看過上篇文章的可以先點擊進去學習一下 簡潔又快速地處理集合——Java8 Stream(上),當然你直接看這篇也可以,不過了解其本身才能更融會貫通哦。

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

本篇文章主要內容:

  • 流基本的常用方法
  • 一種特化形式的流——數值流
  • Optional 類
  • 如何構建一個流
  • collect 方法
  • 並行流相關問題

一. 一般方法

首先我們先創建一個 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

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);

二. 數值流

前面介紹的如
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum); 計算元素總和的方法其中暗含了裝箱成本,map(Person::getAge) 方法過後流變成了 Stream 類型,而每個 Integer 都要拆箱成一個原始類型再進行 sum 方法求和,這樣大大影響了效率。

針對這個問題 Java 8 有良心地引入了數值流 IntStream, DoubleStream, LongStream,這種流中的元素都是原始數據類型,分別是 int,double,long

1. 流與數值流的轉換

流轉換爲數值流

  • mapToInt(T -> int) : return IntStream
  • mapToDouble(T -> double) : return DoubleStream
  • mapToLong(T -> long) : return LongStream
IntStream intStream = list.stream().mapToInt(Person::getAge);

當然如果是下面這樣便會出錯

LongStream longStream = list.stream().mapToInt(Person::getAge);

因爲 getAge 方法返回的是 int 類型(返回的如果是 Integer,一樣可以轉換爲 IntStream)

數值流轉換爲流

很簡單,就一個 boxed

Stream<Integer> stream = intStream.boxed();

2. 數值流方法

下面這些方法作用不用多說,看名字就知道:
- sum()
- max()
- min()
- average() 等…

3. 數值範圍

IntStream 與 LongStream 擁有 range 和 rangeClosed 方法用於數值範圍處理
- IntStream : rangeClosed(int, int) / range(int, int)
- LongStream : rangeClosed(long, long) / range(long, long)

這兩個方法的區別在於一個是閉區間,一個是半開半閉區間:
- rangeClosed(1, 100) :[1, 100]
- range(1, 100) :[1, 100)

我們可以利用 IntStream.rangeClosed(1, 100) 生成 1 到 100 的數值流

110 的數值總和:
IntStream intStream = IntStream.rangeClosed(1, 10);
int sum = intStream.sum();

三. Optional 類

NullPointerException 可以說是每一個 Java 程序員都非常討厭看到的一個詞,針對這個問題, Java 8 引入了一個新的容器類 Optional,可以代表一個值存在或不存在,這樣就不用返回容易出問題的 null。之前文章的代碼中就經常出現這個類,也是針對這個問題進行的改進。

Optional 類比較常用的幾個方法有:
- isPresent() :值存在時返回 true,反之 flase
- get() :返回當前值,若值不存在會拋出異常
- orElse(T) :值存在時返回該值,否則返回 T 的值

Optional 類還有三個特化版本 OptionalInt,OptionalLong,OptionalDouble,剛剛講到的數值流中的 max 方法返回的類型便是這個

Optional 類其中其實還有很多學問,講解它說不定也要開一篇文章,這裏先講那麼多,先知道基本怎麼用就可以。

四. 構建流

之前我們得到一個流是通過一個原始數據源轉換而來,其實我們還可以直接構建得到流。

1. 值創建流

  • Stream.of(T…) : Stream.of(“aa”, “bb”) 生成流
生成一個字符串流
Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
  • Stream.empty() : 生成空流

2. 數組創建流

根據參數的數組類型創建對應的流:
- Arrays.stream(T[ ])
- Arrays.stream(int[ ])
- Arrays.stream(double[ ])
- Arrays.stream(long[ ])

值得注意的是,還可以規定只取數組的某部分,用到的是Arrays.stream(T[], int, int)

只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);

打印 23

3. 文件生成流

Stream<String> stream = Files.lines(Paths.get("data.txt"));

每個元素是給定文件的其中一行

4. 函數生成流

兩個方法:
- iterate : 依次對每個新生成的值應用函數
- generate :接受一個函數,生成一個新的值

Stream.iterate(0, n -> n + 2)
生成流,首元素爲 0,之後依次加 2

Stream.generate(Math :: random)
生成流,爲 01 的隨機雙精度數

Stream.generate(() -> 1)
生成流,元素全爲 1

五. collect 收集數據

coollect 方法作爲終端操作,接受的是一個 Collector 接口參數,能對數據進行一些收集歸總操作

1. 收集

最常用的方法,把流中所有元素收集到一個 List, Set 或 Collection 中
- toList
- toSet
- toCollection

List newlist = list.stream.collect(toList());

2. 彙總

(1)counting

用於計算總和:

long l = list.stream().collect(counting());

沒錯,你應該想到了,下面這樣也可以:

long l = list.stream().count();

推薦第二種

(2)summingInt ,summingLong ,summingDouble

summing,沒錯,也是計算總和,不過這裏需要一個函數參數

計算 Person 年齡總和:

int sum = list.stream().collect(summingInt(Person::getAge));

當然,這個可以也簡化爲:

int sum = list.stream().mapToInt(Person::getAge).sum();

除了上面兩種,其實還可以:

int sum = list.stream().map(Person::getAge).reduce(Interger::sum).get();

推薦第二種

由此可見,函數式編程通常提供了多種方式來完成同一種操作

(3)averagingInt,averagingLong,averagingDouble

看名字就知道,求平均數

Double average = list.stream().collect(averagingInt(Person::getAge));

當然也可以這樣寫

OptionalDouble average = list.stream().mapToInt(Person::getAge).average();

不過要注意的是,這兩種返回的值是不同類型的

(4)summarizingInt,summarizingLong,summarizingDouble

這三個方法比較特殊,比如 summarizingInt 會返回 IntSummaryStatistics 類型

IntSummaryStatistics l = list.stream().collect(summarizingInt(Person::getAge));

IntSummaryStatistics 包含了計算出來的平均值,總數,總和,最值,可以通過下面這些方法獲得相應的數據

3. 取最值

maxBy,minBy 兩個方法,需要一個 Comparator 接口作爲參數

Optional<Person> optional = list.stream().collect(maxBy(comparing(Person::getAge)));

我們也可以直接使用 max 方法獲得同樣的結果

Optional<Person> optional = list.stream().max(comparing(Person::getAge));

4. joining 連接字符串

也是一個比較常用的方法,對流裏面的字符串元素進行連接,其底層實現用的是專門用於字符串連接的 StringBuilder

String s = list.stream().map(Person::getName).collect(joining());

結果:jackmiketom
String s = list.stream().map(Person::getName).collect(joining(","));

結果:jack,mike,tom

joining 還有一個比較特別的重載方法:

String s = list.stream().map(Person::getName).collect(joining(" and ", "Today ", " play games."));

結果:Today jack and mike and tom play games.

即 Today 放開頭,play games. 放結尾,and 在中間連接各個字符串

5. groupingBy 分組

groupingBy 用於將數據分組,最終返回一個 Map 類型

Map<Integer, List<Person>> map = list.stream().collect(groupingBy(Person::getAge));

例子中我們按照年齡 age 分組,每一個 Person 對象中年齡相同的歸爲一組

另外可以看出,Person::getAge 決定 Map 的鍵(Integer 類型),list 類型決定 Map 的值(List 類型)

多級分組

groupingBy 可以接受一個第二參數實現多級分組:

Map<Integer, Map<T, List<Person>>> map = list.stream().collect(groupingBy(Person::getAge, groupBy(...)));

其中返回的 Map 鍵爲 Integer 類型,值爲 Map

按組收集數據

Map<Integer, Integer> map = list.stream().collect(groupingBy(Person::getAge, summingInt(Person::getAge)));

該例子中,我們通過年齡進行分組,然後 summingInt(Person::getAge)) 分別計算每一組的年齡總和(Integer),最終返回一個 Map

groupingBy(Person::getAge)

其實等同於:

groupingBy(Person::getAge, toList())

6. partitioningBy 分區

分區與分組的區別在於,分區是按照 true 和 false 來分的,因此partitioningBy 接受的參數的 lambda 也是 T -> boolean

根據年齡是否小於等於20來分區
Map<Boolean, List<Person>> map = list.stream()
                                     .collect(partitioningBy(p -> p.getAge() <= 20));

打印輸出
{
    false=[Person{name='mike', age=25}, Person{name='tom', age=30}], 
    true=[Person{name='jack', age=20}]
}

同樣地 partitioningBy 也可以添加一個收集器作爲第二參數,進行類似 groupBy 的多重分區等等操作。

六. 並行

之前我就講到了 parallelStream 方法能生成並行流,因此你通常可以使用 parallelStream 來代替 stream 方法,但是並行的性能問題非常值得我們思考

比方說下面這個例子

 int i = Stream.iterate(1, a -> a + 1).limit(100).parallel().reduce(0, Integer::sum);

我們通過這樣一行代碼來計算 1 到 100 的所有數的和,我們使用了 parallel 來實現並行。

但實際上是,這樣的計算,效率是非常低的,比不使用並行還低!一方面是因爲裝箱問題,這個前面也提到過,就不再贅述,還有一方面就是 iterate 方法很難把這些數分成多個獨立塊來並行執行,因此無形之中降低了效率。

流的可分解性

這就說到流的可分解性問題了,使用並行的時候,我們要注意流背後的數據結構是否易於分解。比如衆所周知的 ArrayList 和 LinkedList,明顯前者在分解方面佔優。

我們來看看一些數據源的可分解性情況

數據源 可分解性
ArrayList 極佳
LinkedList
IntStream.range 極佳
Stream.iterate
HashSet
TreeSet

順序性

除了可分解性,和剛剛提到的裝箱問題,還有一點值得注意的是一些操作本身在並行流上的性能就比順序流要差,比如:limit,findFirst,因爲這兩個方法會考慮元素的順序性,而並行本身就是違背順序性的,也是因爲如此 findAny 一般比 findFirst 的效率要高。


相關閱讀

猜你喜歡

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

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