JDK1.8系列文章
- JDK1.8新特性(一):Lambda表達(dá)式
- JDK1.8新特性(二):Optional 類
- JDK1.8新特性(三):Stream
- JDK1.8新特性(四):Maps
- JDK1.8新特性(五):新的日期時(shí)間 API
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í)視頻。