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

上一篇文章我講解 Stream 流的基本原理,以及它的基本方法使用,本篇文章我們繼續講解流的其他操作

沒有看過上篇文章的可以先點擊進去學習一下 簡潔又快速地處理集合——Java8 Stream(上)

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

本篇文章主要內容:

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

一. 數值流

前面介紹的如
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum); 計算元素總和的方法其中暗含了裝箱成本,map(Person::getAge) 方法過后流變成了 Stream<Integer> 類型,而每個 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 的數值流

求 1 到 10 的數值總和:
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);

打印 2 ,3

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)
生成流,為 0 到 1 的隨機雙精度數

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

四. collect 收集數據

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

1. 收集

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

  • toList
  • toSet
  • toCollection
  • toMap
List newlist = list.stream.collect(toList());
//如果 Map 的 Key 重復了,可是會報錯的哦
Map<Integer, Person> map = list.stream().collect(toMap(Person::getAge, p -> p));

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<Person> 類型)

多級分組

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

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

其中返回的 Map 鍵為 Integer 類型,值為 Map<T, List<Person>> 類型,即參數中 groupBy(...) 返回的類型

按組收集數據

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

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

根據這個方法,我們可以知道,前面我們寫的:

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 的多重分區等等操作。

五. 并行

我們通過 list.stream() 將 List 類型轉換為流類型,我們還可以通過 list.parallelStream() 轉換為并行流。因此你通常可以使用 parallelStream 來代替 stream 方法

并行流就是把內容分成多個數據塊,使用不同的線程分別處理每個數據塊的流。這也是流的一大特點,要知道,在 Java 7 之前,并行處理數據集合是非常麻煩的,你得自己去將數據分割開,自己去分配線程,必要時還要確保同步避免競爭。

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 的效率要高。

六. 效率

最后再來談談效率問題,很多人可能聽說過有關 Stream 效率低下的問題。其實,對于一些簡單的操作,比如單純的遍歷,查找最值等等,Stream 的性能的確會低于傳統的循環或者迭代器實現,甚至會低很多。

但是對于復雜的操作,比如一些復雜的對象歸約,Stream 的性能是可以和手動實現的性能匹敵的,在某些情況下使用并行流,效率可能還遠超手動實現。好鋼用在刀刃上,在適合的場景下使用,才能發揮其最大的用處。

函數式接口的出現主要是為了提高編碼開發效率以及增強代碼可讀性;與此同時,在實際的開發中,并非總是要求非常高的性能,因此 Stream 與 lambda 的出現意義還是非常大的。


相關閱讀

猜你喜歡

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容