JDK1.8新特性(三):Stream

JDK1.8系列文章

Stream 是 Java8 中處理集合的關(guān)鍵抽象概念,它可以指定你希望對集合進(jìn)行的操作,可以執(zhí)行非常復(fù)雜的查找、過濾和映射數(shù)據(jù)等操作。使用Stream API 對集合數(shù)據(jù)進(jìn)行操作,就類似于使用 SQL 執(zhí)行的數(shù)據(jù)庫查詢。也可以使用 Stream API 來并行執(zhí)行操作。Stream API 提供了一種高效且易于使用的處理數(shù)據(jù)的方式。

一、什么是 Stream

Stream 中文稱為 “流”,通過將集合轉(zhuǎn)換為這么一種叫做 “流” 的元素序列,通過聲明性方式,能夠?qū)现械拿總€(gè)元素進(jìn)行一系列并行或串行的流水線操作。
換句話說,你只需要告訴流你的要求,流便會(huì)在背后自行根據(jù)要求對元素進(jìn)行處理,而你只需要 “坐享其成”。

二、流操作

整個(gè)流操作就是一條流水線,將元素放在流水線上一個(gè)個(gè)地進(jìn)行處理。
其中數(shù)據(jù)源便是原始集合,然后將如 List 的集合轉(zhuǎn)換為 Stream 類型的流,并對流進(jìn)行一系列的中間操作,比如過濾保留部分元素、對元素進(jìn)行排序、類型轉(zhuǎn)換等;最后再進(jìn)行一個(gè)終端操作,可以把 Stream 轉(zhuǎn)換回集合類型,也可以直接對其中的各個(gè)元素進(jìn)行處理,比如打印、比如計(jì)算總數(shù)、計(jì)算最大值等等
很重要的一點(diǎn)是,很多流操作本身就會(huì)返回一個(gè)流,所以多個(gè)操作可以直接連接起來,我們來看看一條 Stream 操作的代碼:

List<String> list = new ArrayList<>();
// 在list中增加20個(gè)字符串元素
list.add("Lucas");
List<Integer> streamList = list.stream().map(String::length).sorted().limit(10).collect(Collectors.toList());
// stream() 將集合轉(zhuǎn)換為流
// map(String::length) 將原來的 List<String>  轉(zhuǎn)換為 List<Integer>
// sorted() 排序
// limit(10) 保留前10個(gè)元素
// collect(Collectors.toList()) 將流轉(zhuǎn)換回集合

如果是以前,進(jìn)行這么一系列操作,你需要做個(gè)迭代器或者 foreach 循環(huán),然后遍歷,一步步地親力親為地去完成這些操作;但是如果使用流,你便可以直接聲明式地下指令,流會(huì)幫你完成這些操作。

三、流與集合的差異

1、何時(shí)進(jìn)行計(jì)算

一個(gè)集合,它會(huì)包含當(dāng)前數(shù)據(jù)結(jié)構(gòu)中所有的值,你可以隨時(shí)增刪,但是集合里面的元素毫無疑問地都是已經(jīng)計(jì)算好了的。
流則是按需計(jì)算,按照使用者的需要計(jì)算數(shù)據(jù),你可以想象我們通過搜索引擎進(jìn)行搜索,搜索出來的條目并不是全部呈現(xiàn)出來的,而且先顯示最符合的前 10 條或者前 20 條,只有在點(diǎn)擊 “下一頁” 的時(shí)候,才會(huì)再輸出新的 10 條。再比方在線觀看電影和你硬盤里面的電影,也是差不多的道理。

2、迭代方式

Stream的迭代方式是內(nèi)部迭代,集合的迭代方式外部迭代。
我們可以把集合比作一個(gè)工廠的倉庫,一開始工廠比較落后,要對貨物作什么修改,只能工人親自走進(jìn)倉庫對貨物進(jìn)行處理,有時(shí)候還要將處理后的貨物放到一個(gè)新的倉庫里面。在這個(gè)時(shí)期,我們需要親自去做迭代,一個(gè)個(gè)地找到需要的貨物,并進(jìn)行處理,這叫做外部迭代。
后來工廠發(fā)展了起來,配備了流水線作業(yè),只要根據(jù)需求設(shè)計(jì)出相應(yīng)的流水線,然后工人只要把貨物放到流水線上,就可以等著接收成果了,而且流水線還可以根據(jù)要求直接把貨物輸送到相應(yīng)的倉庫。這就叫做內(nèi)部迭代,流水線已經(jīng)幫你把迭代給完成了,你只需要說要干什么就可以了(即設(shè)計(jì)出合理的流水線)。
流和迭代器類似,只能迭代一次。下面代碼中第三行會(huì)報(bào)錯(cuò),因?yàn)榈诙幸呀?jīng)使用過這個(gè)流,這個(gè)流已經(jīng)被消費(fèi)掉了。

List<String> list = new ArrayList<>();
Stream<Integer> stream =list.stream().map(String::length).sorted().limit(10);
List<Integer> newList = stream.collect(Collectors.toList());
List<Integer> newList2 = stream.collect(Collectors.toList());   // 報(bào)錯(cuò)

Java 8 引入 Stream 很大程度是因?yàn)?,流的?nèi)部迭代可以自動(dòng)選擇一種合適你硬件的數(shù)據(jù)表示和并行實(shí)現(xiàn);而以往程序員自己進(jìn)行 foreach 之類的時(shí)候,則需要自己去管理并行等問題。

四、常用方法

首先我們先創(chuàng)建一個(gè) Person 測試類,包含年齡的姓名兩個(gè)變量

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

再創(chuàng)建一個(gè) 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));

1、stream() / parallelStream()

最常用到的方法,將集合轉(zhuǎn)換為流

Stream stream = list.stream();

parallelStream() 是并行流方法,能夠讓數(shù)據(jù)集執(zhí)行并行操作,后面會(huì)更詳細(xì)地講解

2、filter(T -> boolean)

保留 boolean 為 true 的元素

// 保留年齡為 20 的 person 元素
list = list.stream()
            .filter(person -> person.getAge() == 20)
            .collect(Collectors.toList());

打印輸出 [Person{name='jack', age=20}],collect(toList()) 可以把流轉(zhuǎn)換為 List 類型,這個(gè)以后會(huì)講解

3、distinct()

去除重復(fù)元素,這個(gè)方法是通過類的 equals 方法來判斷兩個(gè)元素是否相等的
如例子中的 Person 類,需要先定義好 equals 方法,不然類似 [Person{name='jack', age=20}, Person{name='jack', age=20}] 這樣的情況是不會(huì)處理的

4、sorted() / sorted((T, T) -> int)

如果流中的元素的類實(shí)現(xiàn)了 Comparable 接口,即有自己的排序規(guī)則,那么可以直接調(diào)用 sorted() 方法對元素進(jìn)行排序,如 Stream
反之, 需要調(diào)用 sorted((T, T) -> int) 實(shí)現(xiàn) Comparator 接口

// 根據(jù)年齡大小來比較:
list = list.stream()
        .sorted((p1, p2) -> p1.getAge() - p2.getAge())
        .collect(Collectors.toList());

當(dāng)然這個(gè)可以簡化為

list = list.stream()
        .sorted(Comparator.comparingInt(Person::getAge))
        .collect(Collectors.toList());

5、limit(long n)

返回前 n 個(gè)元素

list = list.stream()
            .limit(2)
            .collect(Collectors.toList());

// 打印輸出 [Person{name='jack', age=20}, Person{name='mike', age=25}]

6、skip(long n)

去除前 n 個(gè)元素

list = list.stream()
            .skip(2)
            .collect(Collectors.toList());

// 打印輸出 [Person{name='tom', age=30}]

tips:

  • 用在 limit(n) 前面時(shí),先去除前 m 個(gè)元素再返回剩余元素的前 n 個(gè)元素
  • limit(n) 用在 skip(m) 前面時(shí),先返回前 n 個(gè)元素再在剩余的 n 個(gè)元素中去除 m 個(gè)元素
list = list.stream()
            .limit(2)
            .skip(1)
            .collect(Collectors.toList());

// 打印輸出 [Person{name='mike', age=25}]

7、map(T -> R)

將流中的每一個(gè)元素 T 映射為 R(類似類型轉(zhuǎn)換)

List<String> newlist = list.stream()
        .map(Person::getName)
        .collect(Collectors.toList());
        
// 打印輸出 [jack, mike, tom]

newlist 里面的元素為 list 中每一個(gè) Person 對象的 name 變量

8、flatMap(T -> Stream)

將流中的每一個(gè)元素 T 映射為一個(gè)流,再把每一個(gè)流連接成為一個(gè)流

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(Collectors.toList());
        
// 打印輸出 [aaa, bbb, ccc, ddd, eee, fff, ggg, hhh, iii]

上面例子中,我們的目的是把 List 中每個(gè)字符串元素以" "分割開,變成一個(gè)新的 List。首先 map 方法分割每個(gè)字符串元素,但此時(shí)流的類型為 Stream<String[ ]>,因?yàn)?split 方法返回的是 String[ ] 類型;所以我們需要使用 flatMap 方法,先使用Arrays::stream將每個(gè) String[ ] 元素變成一個(gè) Stream 流,然后 flatMap 會(huì)將每一個(gè)流連接成為一個(gè)流,最終返回我們需要的 Stream

9、anyMatch(T -> boolean)

流中是否有一個(gè)元素匹配給定的 T -> boolean 條件

// 是否存在一個(gè) 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():找到其中一個(gè)元素 (使用 stream() 時(shí)找到的是第一個(gè)元素;使用 parallelStream() 并行時(shí)找到的是其中一個(gè)元素)
  • findFirst():找到第一個(gè)元素

值得注意的是,這兩個(gè)方法返回的是一個(gè) Optional 對象,它是一個(gè)容器類,代表一個(gè)值存在或不存在

13、reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)

用于組合流中的元素,如求和,求積,求最大值等

// 計(jì)算年齡總和:
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 第一個(gè)參數(shù) 0 代表起始值為 0,lambda (a, b) -> a + b 即將兩值相加產(chǎn)生一個(gè)新值,同樣地:

// 計(jì)算年齡總乘積:
int sum = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);

當(dāng)然也可以

Optional<Integer> sum = list.stream().map(Person::getAge).reduce(Integer::sum);

即不接受任何起始值,但因?yàn)闆]有初始值,需要考慮結(jié)果可能不存在的情況,因此返回的是 Optional 類型

14、count()

返回流中元素個(gè)數(shù),結(jié)果為 long 類型

15、collect()

收集方法,我們很常用的是 collect(toList()),當(dāng)然還有 collect(toSet()) 等,參數(shù)是一個(gè)收集器接口

16、forEach()

對元素進(jìn)行迭代

// 打印各個(gè)元素:
list.stream().forEach(System.out::println);

五、數(shù)據(jù)流

前面介紹的如 int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum); 計(jì)算元素總和的方法其中暗含了裝箱成本,map(Person::getAge) 方法過后流變成了 Stream 類型,而每個(gè) Integer 都要拆箱成一個(gè)原始類型再進(jìn)行 sum 方法求和,這樣大大影響了效率。
針對這個(gè)問題 Java 8 引入了數(shù)值流 IntStream, DoubleStream, LongStream,這種流中的元素都是原始數(shù)據(jù)類型,分別是 int,double,long

1、流與數(shù)值流的轉(zhuǎn)換

1.1、流轉(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)然如果是下面這樣便會(huì)出錯(cuò)

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

因?yàn)?getAge 方法返回的是 int 類型(返回的如果是 Integer,一樣可以轉(zhuǎn)換為 IntStream)

1.2、數(shù)值流轉(zhuǎn)換為流

很簡單,就一個(gè) 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)

這兩個(gè)方法的區(qū)別在于一個(gè)是閉區(qū)間,一個(gè)是半開半閉區(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();

六、構(gòu)建流

之前我們得到一個(gè)流是通過一個(gè)原始數(shù)據(jù)源轉(zhuǎn)換而來,其實(shí)我們還可以直接構(gòu)建得到流。

1、值創(chuàng)建流

Stream.of(T...) : Stream.of("aa", "bb") 生成流

// 生成一個(gè)字符串流
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);

3、文件生成流

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

每個(gè)元素是給定文件的其中一行

4、函數(shù)生成流

兩個(gè)方法:

  • iterate : 依次對每個(gè)新生成的值應(yīng)用函數(shù)
  • generate :接受一個(gè)函數(shù),生成一個(gè)新的值
Stream.iterate(0, n -> n + 2);
// 生成流,首元素為 0,之后依次加 2

Stream.generate(Math :: random);
// 生成流,為 0 到 1 的隨機(jī)雙精度數(shù)

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

七、collect 收集數(shù)據(jù)

coollect 方法作為終端操作,接受的是一個(gè) Collector 接口參數(shù),能對數(shù)據(jù)進(jìn)行一些收集歸總操作

1、收集

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

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

2、匯總

  • 計(jì)算總數(shù) count()
  • 計(jì)算總和 sum()
  • 求平均數(shù) average()
// 計(jì)算總數(shù)
long size = list.stream().count();
// 計(jì)算總和
int sum = list.stream().mapToInt(Person::getAge).sum();
// 求平均數(shù)
Double average = list.stream().collect(Collectors.averagingInt(Person::getAge));
  • summarizingInt,summarizingLong,summarizingDouble這三個(gè)方法比較特殊,比如 summarizingInt 會(huì)返回 IntSummaryStatistics 類型
IntSummaryStatistics l = list.stream().collect(Collectors.summarizingInt(Person::getAge));

IntSummaryStatistics 包含了計(jì)算出來的平均值,總數(shù),總和,最值,可以通過下面這些方法獲得相應(yīng)的數(shù)據(jù),getAverage()、getCount()、getMax()、getMin()、getSum()。

3、取最值

maxBy,minBy 兩個(gè)方法,需要一個(gè) Comparator 接口作為參數(shù)

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

4、joining 連接字符串

也是一個(gè)比較常用的方法,對流里面的字符串元素進(jìn)行連接,其底層實(shí)現(xiàn)用的是專門用于字符串連接的 StringBuilder

String s = list.stream().map(Person::getName).collect(Collectors.joining(","));
// 結(jié)果:jack,mike,tom

joining 還有一個(gè)比較特別的重載方法:

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

即 Today 放開頭,play games. 放結(jié)尾,and 在中間連接各個(gè)字符串

5、groupingBy 分組

groupingBy 用于將數(shù)據(jù)分組,最終返回一個(gè) Map 類型

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

例子中我們按照年齡 age 分組,每一個(gè) Person 對象中年齡相同的歸為一組
另外可以看出,Person::getAge 決定 Map 的鍵(Integer 類型),list 類型決定 Map 的值(List 類型)
(一)多級分組groupingBy 可以接受一個(gè)第二參數(shù)實(shí)現(xiàn)多級分組:

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

其中返回的 Map 鍵為 Integer 類型,值為 Map<String, List> 類型,即參數(shù)中 groupBy(...) 返回的類型
(二)按組收集數(shù)據(jù)

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

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

6、partitioningBy 分區(qū)

分區(qū)與分組的區(qū)別在于,分區(qū)是按照 true 和 false 來分的,因此partitioningBy 接受的參數(shù)的 lambda 也是 T -> boolean

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

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

同樣地 partitioningBy 也可以添加一個(gè)收集器作為第二參數(shù),進(jìn)行類似 groupBy 的多重分區(qū)等等操作。

八、并行

我們通過 list.stream() 將 List 類型轉(zhuǎn)換為流類型,我們還可以通過 list.parallelStream() 轉(zhuǎn)換為并行流。因此你通??梢允褂?parallelStream 來代替 stream 方法
并行流就是把內(nèi)容分成多個(gè)數(shù)據(jù)塊,使用不同的線程分別處理每個(gè)數(shù)據(jù)塊的流。這也是流的一大特點(diǎn),要知道,在 Java 7 之前,并行處理數(shù)據(jù)集合是非常麻煩的,你得自己去將數(shù)據(jù)分割開,自己去分配線程,必要時(shí)還要確保同步避免競爭。
Stream 讓程序員能夠比較輕易地實(shí)現(xiàn)對數(shù)據(jù)集合的并行處理,但要注意的是,不是所有情況的適合,有些時(shí)候并行甚至比順序進(jìn)行效率更低,而有時(shí)候因?yàn)榫€程安全問題,還可能導(dǎo)致數(shù)據(jù)的處理錯(cuò)誤,這些我會(huì)在下一篇文章中講解。
比方說下面這個(gè)例子

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

我們通過這樣一行代碼來計(jì)算 1 到 100 的所有數(shù)的和,我們使用了 parallel 來實(shí)現(xiàn)并行。
但實(shí)際上是,這樣的計(jì)算,效率是非常低的,比不使用并行還低!一方面是因?yàn)檠b箱問題,這個(gè)前面也提到過,就不再贅述,還有一方面就是 iterate 方法很難把這些數(shù)分成多個(gè)獨(dú)立塊來并行執(zhí)行,因此無形之中降低了效率。

流的可分解性

這就說到流的可分解性問題了,使用并行的時(shí)候,我們要注意流背后的數(shù)據(jù)結(jié)構(gòu)是否易于分解。比如眾所周知的 ArrayList 和 LinkedList,明顯前者在分解方面占優(yōu)。
我們來看看一些數(shù)據(jù)源的可分解性情況

數(shù)據(jù)源 可分解性
ArrayList 極佳
LinkedList
IntStream.range 極佳
Stream.iterate
HashSet
TreeSet

九、效率

最后再來談?wù)勑蕟栴},很多人可能聽說過有關(guān)Stream 效率底下的問題。其實(shí),對于一些簡單的操作,比如單純的遍歷,查找最值等等,Stream 的性能的確會(huì)低于傳統(tǒng)的循環(huán)或者迭代器實(shí)現(xiàn),甚至?xí)秃芏唷?br> 但是對于復(fù)雜的操作,比如一些復(fù)雜的對象歸約,Stream 的性能是可以和手動(dòng)實(shí)現(xiàn)的性能匹敵的,在某些情況下使用并行流,效率可能還遠(yuǎn)超手動(dòng)實(shí)現(xiàn)。好鋼用在刀刃上,在適合的場景下使用,才能發(fā)揮其最大的用處。
函數(shù)式接口的出現(xiàn)主要是為了提高編碼開發(fā)效率以及增強(qiáng)代碼可讀性;與此同時(shí),在實(shí)際的開發(fā)中,并非總是要求非常高的性能,因此 Stream 與 lambda 的出現(xiàn)意義還是非常大的。

本文轉(zhuǎn)載自:http://www.itdecent.cn/p/e429c517e9cb

十、公眾號

如果大家想要第一時(shí)間看到我更新的 Java 方向?qū)W習(xí)文章,可以關(guān)注一下公眾號【Lucas的咖啡店】。所有學(xué)習(xí)文章公眾號首發(fā),請各位大大掃碼關(guān)注一下哦!

Java 8 新特性學(xué)習(xí)視頻請關(guān)注公眾號,私信【Java8】即可免費(fèi)無套路獲取學(xué)習(xí)視頻。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容