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

上一篇文章我講解 Stream 流的基本原理,以及它的基本方法使用,本篇文章咱們繼續講解流的其餘操做程序員

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

本篇文章主要內容:數組

  1. 一種特化形式的流——數值流
  2. Optional 類
  3. 如何構建一個流
  4. collect 方法
  5. 並行流相關問題

一. 數值流安全

前面介紹的如微信

int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
複製代碼

計算元素總和的方法其中暗含了裝箱成本,map(Person::getAge) 方法事後流變成了 Stream類型,而每一個 Integer 都要拆箱成一個原始類型再進行 sum 方法求和,這樣大大影響了效率。數據結構

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

  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();
複製代碼
  1. 數值流方法

下面這些方法做用不用多說,看名字就知道:

  • sum()
  • max()
  • min()
  • average() 等...
  1. 數值範圍

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() : 生成空流

  1. 數組建立流

根據參數的數組類型建立對應的流:

  • 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
複製代碼
  1. 文件生成流

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

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

  1. 函數生成流

兩個方法:

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

  1. 彙總

(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 包含了計算出來的平均值,總數,總和,最值,能夠經過下面這些方法得到相應的數據

getAverage()                                         double
getCount()                                           long
getMax()										 int
getMin()										 int
getSum()										 long
複製代碼
  1. 取最值

maxBy,minBy 兩個方法,須要一個 Comparator 接口做爲參數

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

咱們也能夠直接使用 max 方法得到一樣的結果

Optional<Person> optional = list.stream().max(comparing(Person::getAge));
複製代碼
  1. joining 鏈接字符串

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

String s = list.stream().map(Person::getName).collect(Collectors.joining());
//結果:jackmiketom
String s = list.stream().map(Person::getName).collect(Collectors.joining(","));
//結果:jack,mike,tom
複製代碼

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

String s = list.stream().map(Person::getName).collect(Collectors.joining(" and ", "Today ", " play games."));
//結果:Today jack and mike and tom play games.
複製代碼

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

  1. 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, groupingBy(...)));
複製代碼

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

按組收集數據

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())
複製代碼
  1. 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 的出現意義仍是很是大的。

做者:Howie_Y

主頁:www.jianshu.com/u/79638e5f0743


歡迎關注公衆號:

公衆號微信
相關文章
相關標籤/搜索