上一篇文章我講解 Stream 流的基本原理,以及它的基本方法使用,本篇文章我們繼續(xù)講解流的其他操作
沒有看過上篇文章的可以先點擊進去學(xué)習(xí)一下 簡潔又快速地處理集合——Java8 Stream(上)
值得注意的是:學(xué)習(xí) Stream 之前必須先學(xué)習(xí) lambda 的相關(guān)知識。本文也假設(shè)讀者已經(jīng)掌握 lambda 的相關(guān)知識。
本篇文章主要內(nèi)容:
- 一種特化形式的流——數(shù)值流
- Optional 類
- 如何構(gòu)建一個流
- collect 方法
- 并行流相關(guān)問題
一. 數(shù)值流
前面介紹的如
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum); 計算元素總和的方法其中暗含了裝箱成本,map(Person::getAge) 方法過后流變成了 Stream<Integer> 類型,而每個 Integer 都要拆箱成一個原始類型再進行 sum 方法求和,這樣大大影響了效率。
針對這個問題 Java 8 有良心地引入了數(shù)值流 IntStream, DoubleStream, LongStream,這種流中的元素都是原始數(shù)據(jù)類型,分別是 int,double,long
1. 流與數(shù)值流的轉(zhuǎn)換
流轉(zhuǎn)換為數(shù)值流
- mapToInt(T -> int) : return IntStream
- mapToDouble(T -> double) : return DoubleStream
- mapToLong(T -> long) : return LongStream
IntStream intStream = list.stream().mapToInt(Person::getAge);
當(dāng)然如果是下面這樣便會出錯
LongStream longStream = list.stream().mapToInt(Person::getAge);
因為 getAge 方法返回的是 int 類型(返回的如果是 Integer,一樣可以轉(zhuǎn)換為 IntStream)
數(shù)值流轉(zhuǎn)換為流
很簡單,就一個 boxed
Stream<Integer> stream = intStream.boxed();
2. 數(shù)值流方法
下面這些方法作用不用多說,看名字就知道:
- sum()
- max()
- min()
- average() 等...
3. 數(shù)值范圍
IntStream 與 LongStream 擁有 range 和 rangeClosed 方法用于數(shù)值范圍處理
- IntStream : rangeClosed(int, int) / range(int, int)
- LongStream : rangeClosed(long, long) / range(long, long)
這兩個方法的區(qū)別在于一個是閉區(qū)間,一個是半開半閉區(qū)間:
- rangeClosed(1, 100) :[1, 100]
- range(1, 100) :[1, 100)
我們可以利用 IntStream.rangeClosed(1, 100) 生成 1 到 100 的數(shù)值流
求 1 到 10 的數(shù)值總和:
IntStream intStream = IntStream.rangeClosed(1, 10);
int sum = intStream.sum();
二. Optional 類
NullPointerException 可以說是每一個 Java 程序員都非常討厭看到的一個詞,針對這個問題, Java 8 引入了一個新的容器類 Optional,可以代表一個值存在或不存在,這樣就不用返回容易出問題的 null。之前文章的代碼中就經(jīng)常出現(xiàn)這個類,也是針對這個問題進行的改進。
Optional 類比較常用的幾個方法有:
- isPresent() :值存在時返回 true,反之 flase
- get() :返回當(dāng)前值,若值不存在會拋出異常
- orElse(T) :值存在時返回該值,否則返回 T 的值
Optional 類還有三個特化版本 OptionalInt,OptionalLong,OptionalDouble,剛剛講到的數(shù)值流中的 max 方法返回的類型便是這個
Optional 類其中其實還有很多學(xué)問,講解它說不定也要開一篇文章,這里先講那么多,先知道基本怎么用就可以。
三. 構(gòu)建流
之前我們得到一個流是通過一個原始數(shù)據(jù)源轉(zhuǎn)換而來,其實我們還可以直接構(gòu)建得到流。
1. 值創(chuàng)建流
- Stream.of(T...) : Stream.of("aa", "bb") 生成流
生成一個字符串流
Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
- Stream.empty() : 生成空流
2. 數(shù)組創(chuàng)建流
根據(jù)參數(shù)的數(shù)組類型創(chuàng)建對應(yīng)的流:
- Arrays.stream(T[ ])
- Arrays.stream(int[ ])
- Arrays.stream(double[ ])
- Arrays.stream(long[ ])
值得注意的是,還可以規(guī)定只取數(shù)組的某部分,用到的是Arrays.stream(T[], int, int)
只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);
打印 2 ,3
3. 文件生成流
Stream<String> stream = Files.lines(Paths.get("data.txt"));
每個元素是給定文件的其中一行
4. 函數(shù)生成流
兩個方法:
- iterate : 依次對每個新生成的值應(yīng)用函數(shù)
- generate :接受一個函數(shù),生成一個新的值
Stream.iterate(0, n -> n + 2)
生成流,首元素為 0,之后依次加 2
Stream.generate(Math :: random)
生成流,為 0 到 1 的隨機雙精度數(shù)
Stream.generate(() -> 1)
生成流,元素全為 1
四. collect 收集數(shù)據(jù)
coollect 方法作為終端操作,接受的是一個 Collector 接口參數(shù),能對數(shù)據(jù)進行一些收集歸總操作
1. 收集
最常用的方法,把流中所有元素收集到一個 List, Set 或 Collection 中
- toList
- toSet
- toCollection
- toMap
List newlist = list.stream.collect(toList());
//如果 Map 的 Key 重復(fù)了,可是會報錯的哦
Map<Integer, Person> map = list.stream().collect(toMap(Person::getAge, p -> p));
2. 匯總
(1)counting
用于計算總和:
long l = list.stream().collect(counting());
沒錯,你應(yīng)該想到了,下面這樣也可以:
long l = list.stream().count();
推薦第二種
(2)summingInt ,summingLong ,summingDouble
summing,沒錯,也是計算總和,不過這里需要一個函數(shù)參數(shù)
計算 Person 年齡總和:
int sum = list.stream().collect(summingInt(Person::getAge));
當(dāng)然,這個可以也簡化為:
int sum = list.stream().mapToInt(Person::getAge).sum();
除了上面兩種,其實還可以:
int sum = list.stream().map(Person::getAge).reduce(Interger::sum).get();
推薦第二種
由此可見,函數(shù)式編程通常提供了多種方式來完成同一種操作
(3)averagingInt,averagingLong,averagingDouble
看名字就知道,求平均數(shù)
Double average = list.stream().collect(averagingInt(Person::getAge));
當(dāng)然也可以這樣寫
OptionalDouble average = list.stream().mapToInt(Person::getAge).average();
不過要注意的是,這兩種返回的值是不同類型的
(4)summarizingInt,summarizingLong,summarizingDouble
這三個方法比較特殊,比如 summarizingInt 會返回 IntSummaryStatistics 類型
IntSummaryStatistics l = list.stream().collect(summarizingInt(Person::getAge));
IntSummaryStatistics 包含了計算出來的平均值,總數(shù),總和,最值,可以通過下面這些方法獲得相應(yīng)的數(shù)據(jù)

3. 取最值
maxBy,minBy 兩個方法,需要一個 Comparator 接口作為參數(shù)
Optional<Person> optional = list.stream().collect(maxBy(comparing(Person::getAge)));
我們也可以直接使用 max 方法獲得同樣的結(jié)果
Optional<Person> optional = list.stream().max(comparing(Person::getAge));
4. joining 連接字符串
也是一個比較常用的方法,對流里面的字符串元素進行連接,其底層實現(xiàn)用的是專門用于字符串連接的 StringBuilder
String s = list.stream().map(Person::getName).collect(joining());
結(jié)果:jackmiketom
String s = list.stream().map(Person::getName).collect(joining(","));
結(jié)果:jack,mike,tom
joining 還有一個比較特別的重載方法:
String s = list.stream().map(Person::getName).collect(joining(" and ", "Today ", " play games."));
結(jié)果:Today jack and mike and tom play games.
即 Today 放開頭,play games. 放結(jié)尾,and 在中間連接各個字符串
5. groupingBy 分組
groupingBy 用于將數(shù)據(jù)分組,最終返回一個 Map 類型
Map<Integer, List<Person>> map = list.stream().collect(groupingBy(Person::getAge));
例子中我們按照年齡 age 分組,每一個 Person 對象中年齡相同的歸為一組
另外可以看出,Person::getAge 決定 Map 的鍵(Integer 類型),list 類型決定 Map 的值(List<Person> 類型)
多級分組
groupingBy 可以接受一個第二參數(shù)實現(xiàn)多級分組:
Map<Integer, Map<T, List<Person>>> map = list.stream().collect(groupingBy(Person::getAge, groupingBy(...)));
其中返回的 Map 鍵為 Integer 類型,值為 Map<T, List<Person>> 類型,即參數(shù)中 groupBy(...) 返回的類型
按組收集數(shù)據(jù)
Map<Integer, Integer> map = list.stream().collect(groupingBy(Person::getAge, summingInt(Person::getAge)));
該例子中,我們通過年齡進行分組,然后 summingInt(Person::getAge)) 分別計算每一組的年齡總和(Integer),最終返回一個 Map<Integer, Integer>
根據(jù)這個方法,我們可以知道,前面我們寫的:
groupingBy(Person::getAge)
其實等同于:
groupingBy(Person::getAge, toList())
6. partitioningBy 分區(qū)
分區(qū)與分組的區(qū)別在于,分區(qū)是按照 true 和 false 來分的,因此partitioningBy 接受的參數(shù)的 lambda 也是 T -> boolean
根據(jù)年齡是否小于等于20來分區(qū)
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 也可以添加一個收集器作為第二參數(shù),進行類似 groupBy 的多重分區(qū)等等操作。
五. 并行
我們通過 list.stream() 將 List 類型轉(zhuǎn)換為流類型,我們還可以通過 list.parallelStream() 轉(zhuǎn)換為并行流。因此你通??梢允褂?parallelStream 來代替 stream 方法
并行流就是把內(nèi)容分成多個數(shù)據(jù)塊,使用不同的線程分別處理每個數(shù)據(jù)塊的流。這也是流的一大特點,要知道,在 Java 7 之前,并行處理數(shù)據(jù)集合是非常麻煩的,你得自己去將數(shù)據(jù)分割開,自己去分配線程,必要時還要確保同步避免競爭。
Stream 讓程序員能夠比較輕易地實現(xiàn)對數(shù)據(jù)集合的并行處理,但要注意的是,不是所有情況的適合,有些時候并行甚至比順序進行效率更低,而有時候因為線程安全問題,還可能導(dǎo)致數(shù)據(jù)的處理錯誤,這些我會在下一篇文章中講解。
比方說下面這個例子
int i = Stream.iterate(1, a -> a + 1).limit(100).parallel().reduce(0, Integer::sum);
我們通過這樣一行代碼來計算 1 到 100 的所有數(shù)的和,我們使用了 parallel 來實現(xiàn)并行。
但實際上是,這樣的計算,效率是非常低的,比不使用并行還低!一方面是因為裝箱問題,這個前面也提到過,就不再贅述,還有一方面就是 iterate 方法很難把這些數(shù)分成多個獨立塊來并行執(zhí)行,因此無形之中降低了效率。
流的可分解性
這就說到流的可分解性問題了,使用并行的時候,我們要注意流背后的數(shù)據(jù)結(jié)構(gòu)是否易于分解。比如眾所周知的 ArrayList 和 LinkedList,明顯前者在分解方面占優(yōu)。
我們來看看一些數(shù)據(jù)源的可分解性情況
| 數(shù)據(jù)源 | 可分解性 |
|---|---|
| ArrayList | 極佳 |
| LinkedList | 差 |
| IntStream.range | 極佳 |
| Stream.iterate | 差 |
| HashSet | 好 |
| TreeSet | 好 |
順序性
除了可分解性,和剛剛提到的裝箱問題,還有一點值得注意的是一些操作本身在并行流上的性能就比順序流要差,比如:limit,findFirst,因為這兩個方法會考慮元素的順序性,而并行本身就是違背順序性的,也是因為如此 findAny 一般比 findFirst 的效率要高。
六. 效率
最后再來談?wù)勑蕟栴},很多人可能聽說過有關(guān) Stream 效率低下的問題。其實,對于一些簡單的操作,比如單純的遍歷,查找最值等等,Stream 的性能的確會低于傳統(tǒng)的循環(huán)或者迭代器實現(xiàn),甚至?xí)秃芏唷?/p>
但是對于復(fù)雜的操作,比如一些復(fù)雜的對象歸約,Stream 的性能是可以和手動實現(xiàn)的性能匹敵的,在某些情況下使用并行流,效率可能還遠超手動實現(xiàn)。好鋼用在刀刃上,在適合的場景下使用,才能發(fā)揮其最大的用處。
函數(shù)式接口的出現(xiàn)主要是為了提高編碼開發(fā)效率以及增強代碼可讀性;與此同時,在實際的開發(fā)中,并非總是要求非常高的性能,因此 Stream 與 lambda 的出現(xiàn)意義還是非常大的。
相關(guān)閱讀
猜你喜歡
- 你必須搞清楚的String,StringBuilder,StringBuffer
- 分享一些 Java 后端的個人干貨
- 教你 Shiro + SpringBoot 整合 JWT
- 教你 Shiro 整合 SpringBoot,避開各種坑