到今天了你還不會(huì)集合的Stream操作嗎?你要out了
Java8的兩個(gè)重大改變,一個(gè)是Lambda表達(dá)式,另一個(gè)就是本節(jié)要講的Stream API表達(dá)式。Stream 是Java8中處理集合的關(guān)鍵抽象概念,它可以對(duì)集合進(jìn)行非常復(fù)雜的查找、過濾、篩選等操作,在新版的JPA中,也已經(jīng)加入了Stream。
一. Stream操作步驟
1.1 Stream有如下三個(gè)操作步驟:
第一步:創(chuàng)建流
從一個(gè)數(shù)據(jù)源,如集合、數(shù)組中獲取流。
第二步:進(jìn)行中間操作
一個(gè)流后面可以跟0個(gè)或多個(gè)中間操作,多個(gè)中間操作可以連接起來形成一條流水線。中間操作通常在沒有結(jié)束操作之前是不會(huì)被觸發(fā)的。
第三步:獲取結(jié)果(結(jié)束操作)
一個(gè)流只能有一個(gè)結(jié)束操作,當(dāng)這個(gè)操作執(zhí)行后,前面的中間操作會(huì)被觸發(fā),此時(shí)流就再無法被使用。Stream中幾乎所有返回是void方法都是結(jié)束操作。
1.2 Stream的特征
第一步:創(chuàng)建流
- 流不會(huì)存儲(chǔ)值,通過管道的方式獲取值。
- 對(duì)流的操作會(huì)生成一個(gè)結(jié)果,不過并不會(huì)修改底層的數(shù)據(jù)源
- 一個(gè)流只能使用一次
1.3 入門小案例
假設(shè)有一個(gè)Person類和一個(gè)Person列表,現(xiàn)在有兩個(gè)需求:1)找到年齡大于18歲的人并輸出;2)找出所有中國人的數(shù)量。
@Data
class Person {
private String name;
private Integer age;
private String country;
private char sex;
public Person(String name, Integer age, String country, char sex) {
this.name = name;
this.age = age;
this.country = country;
this.sex = sex;
}
}
List<Person> personList = new ArrayList<>();
personList.add(new Person("歐陽雪",18,"中國",'F'));
personList.add(new Person("Tom",24,"美國",'M'));
personList.add(new Person("Harley",22,"英國",'F'));
personList.add(new Person("向天笑",20,"中國",'M'));
personList.add(new Person("李康",22,"中國",'M'));
personList.add(new Person("小梅",20,"中國",'F'));
personList.add(new Person("何雪",21,"中國",'F'));
personList.add(new Person("李康",22,"中國",'M'));
在JDK8以前,我們可以通過遍歷列表來完成。但是在有了Stream API后,可以這樣來實(shí)現(xiàn):
public static void main(String[] args) {
// 1)找到年齡大于18歲的人并輸出;
personList.stream().filter((p) -> p.getAge() > 18).forEach(System.out::println);
System.out.println("-------------------------------------------");
// 2)找出所有中國人的數(shù)量
long chinaPersonNum = personList.stream().filter((p) -> p.getCountry().equals("中國")).count();
System.out.println("中國人有:" + chinaPersonNum + "個(gè)");
}
輸出結(jié)果:
在這個(gè)例子中,personList.stream()是創(chuàng)建流,filter()屬于中間操作,forEach、count()是終止操作。
二. 創(chuàng)建流
2.1 創(chuàng)建有限流
有限流的意思就是創(chuàng)建出來的流是基于一個(gè)已經(jīng)存在的數(shù)據(jù)源,也就是說流在創(chuàng)建的時(shí)候數(shù)據(jù)源的個(gè)數(shù)已經(jīng)基本確定了。
Stream.of():Stream類的靜態(tài)方法。Collection實(shí)例.stream():返回此集合作為數(shù)據(jù)源的流。Collection實(shí)例.parallelStream():返回此集合作為數(shù)據(jù)源的并行流。Arrays.stream():數(shù)組轉(zhuǎn)換為流。
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
String[] arr=new String[15];
Stream<String> stream2 = Arrays.stream(arr);
Stream<String> stream3 = Stream.of("a", "b", "c");
2.2 創(chuàng)建無限流
無限流相對(duì)于有限流,并沒有一個(gè)特定定的數(shù)據(jù)源,它是通過循環(huán)執(zhí)行"元素生成器"來不斷產(chǎn)生新元素的。這種方式在日常開發(fā)中相對(duì)少見。
Stream.iterate(T seed, UnaryOperator<T> f):創(chuàng)建一個(gè)無限但有序的流 ,seed傳入的是迭代器的種子(起始值),f傳入的是迭代函數(shù)。
//這個(gè)流從0開始,每一個(gè)元素加3
Stream<Integer> stream4 = Stream.iterate(0, i -> i + 3);
//循環(huán)打印流中的元素(如果不停止運(yùn)行,程序會(huì)無限輸出下去)
stream4.forEach(System.out::println);
//----------上述Lambda表達(dá)式實(shí)際上是下列語法的一個(gè)簡(jiǎn)化寫法,后文不再提供Lambda表達(dá)式轉(zhuǎn)換,如果看不懂的話可以先去熟悉Lambda表達(dá)式的用法------
Stream.iterate(0, new UnaryOperator<Integer>() {
@Override
public Integer apply(Integer i) {
return i + 3;
}
});
Stream.generate(Supplier<T> s):傳入的是一個(gè)“元素生成器”,Supplier是一個(gè)函數(shù)式接口,它用于在無輸入?yún)?shù)的情況下,返回一個(gè)結(jié)果值。
//創(chuàng)建一個(gè)無限流,這個(gè)流中每一個(gè)元素都是Math.random()方法生成的
Stream<Double> stream5 = Stream.generate(() -> Math.random());
stream5.forEach(System.out::println);
//----------上述Lambda表達(dá)式由于只調(diào)用了一個(gè) Math.random()方法,實(shí)際上能夠更加精簡(jiǎn)----
Stream<Double> stream5 = Stream.generate(Math::random);
三. Stream中間操作
3.1 篩選與切片
- filter:從流中排除某些操作;
- limit(n):截?cái)嗔?,使其元素不超過給定對(duì)象
- skip(n):跳過元素,返回一個(gè)扔掉了前n個(gè)元素的流,若流中元素不足n個(gè),則返回一個(gè)空流,與limit(n)互補(bǔ)
- distinct:篩選,通過流所生成元素的hashCode()和equals()去除重復(fù)元素。
3.1.1 filter
//保留流中person.getAge()==20的元素
personList.stream().filter(person -> person.getAge() == 20).forEach(System.out::println);
輸出結(jié)果為:
Person(name=向天笑, age=20, country=中國, sex=M)
Person(name=小梅, age=20, country=中國, sex=F)
3.1.2 limit
personList.stream().limit(2).forEach(System.out::println);
輸出結(jié)果為:
Person(name=歐陽雪, age=18, country=中國, sex=F)
Person(name=Tom, age=24, country=美國, sex=M)
3.1.3 skip
personList.stream().skip(1).forEach(System.out::println);
輸出結(jié)果為:
Person(name=Harley, age=22, country=英國, sex=F)
Person(name=向天笑, age=20, country=中國, sex=M)
Person(name=李康, age=22, country=中國, sex=M)
Person(name=小梅, age=20, country=中國, sex=F)
Person(name=何雪, age=21, country=中國, sex=F)
Person(name=李康, age=22, country=中國, sex=M)
3.1.4 distinct
personList.stream().distinct().forEach(System.out::println);
輸出結(jié)果為:
Person(name=歐陽雪, age=18, country=中國, sex=F)
Person(name=Tom, age=24, country=美國, sex=M)
Person(name=Harley, age=22, country=英國, sex=F)
Person(name=向天笑, age=20, country=中國, sex=M)
Person(name=李康, age=22, country=中國, sex=M)
Person(name=小梅, age=20, country=中國, sex=F)
Person(name=何雪, age=21, country=中國, sex=F)
3.2 映射
3.2.1 map
map映射是接收一個(gè)Function接口的實(shí)例,它將Stream中的所有元素依次傳入進(jìn)去,Function.apply方法將原數(shù)據(jù)轉(zhuǎn)換成其他形式的數(shù)據(jù)。
例一:
假如,我們需要將List<String>所有元素轉(zhuǎn)化為大寫,我們可以這么做:
Stream<String> stream = Stream.of("aaa", "bbb", "ccc", "ddd");
stream.map(new Function<String, String>() {
@Override
public String apply(String s) {
return s.toUpperCase();
}
}).forEach(System.out::println);
//上面的匿名內(nèi)部類可以精簡(jiǎn)為Lambda表達(dá)式形式
stream.map(s -> s.toUpperCase()).forEach(System.out::println);
//采用方法引用,更進(jìn)一步精簡(jiǎn)Lambda表達(dá)式
stream.map(String::toUpperCase).forEach(System.out::println);
流中的每一個(gè)元素都經(jīng)過了Function實(shí)例apply方法的處理,將其轉(zhuǎn)換為了大寫。
例二:
假如我們需要取出List<Person>中每一個(gè)Person中的姓名取出來并打印出來:
List<Person> personList = new ArrayList<>();
personList.add(new Person("歐陽雪", 18, "中國", 'F'));
personList.add(new Person("Tom", 24, "美國", 'M'));
personList.add(new Person("Harley", 22, "英國", 'F'));
personList.add(new Person("向天笑", 20, "中國", 'M'));
personList.add(new Person("李康", 22, "中國", 'M'));
personList.add(new Person("小梅", 20, "中國", 'F'));
personList.add(new Person("何雪", 21, "中國", 'F'));
personList.add(new Person("李康", 22, "中國", 'M'));
personList.stream().map(new Function<Person, String>() {
@Override
public String apply(Person person) {
return person.getName();
}
}).forEach(System.out::println);
//上面的匿名內(nèi)部類可以精簡(jiǎn)為Lambda表達(dá)式形式
personList.stream().map(person -> person.getName()).forEach(System.out::println);
//采用方法引用,更進(jìn)一步精簡(jiǎn)Lambda表達(dá)式
stream.map(Person::getName).forEach(System.out::println);
3.2.2 flatMap
flatMap接收一個(gè)函數(shù)作為參數(shù),將流中的每一個(gè)值轉(zhuǎn)換成另一個(gè)流,然后把所有流合并在一起。
例一:
我們需要將List<String>中的每一個(gè)字符按順序打印出來。我們先試著用map映射達(dá)到需求:
//使用map也能實(shí)現(xiàn)這個(gè)需求
@Test
public void testFlatMap() {
Stream<String> stream = Stream.of("aaa", "bbb", "ccc", "ddd");
Stream<Stream<Character>> streamStream = stream.map(s -> toCharacterStream(s));
streamStream.forEach(s->s.forEach(System.out::println));
}
public static Stream<Character> toCharacterStream(String str) {
List<Character> list = new ArrayList<>();
for (char c : str.toCharArray()) {
list.add(c);
}
return list.stream();
}
我們通過map()將原先流中每一個(gè)String映射為Stream<Character>然后重新放入原先的流中,此時(shí)流中的每一個(gè)元素都是一個(gè)Stream<Character>,然后我們遍歷外層流得到每一個(gè)子流,然后子流forEach輸出每一個(gè)字符。
可以看到使用map映射的時(shí)候,如果返回Stream,那這些Stream仍然是相互獨(dú)立的;但是flatMap映射會(huì)將返回的流合并為一個(gè)流。
@Test
public void testFlatMap2() {
Stream<String> stream = Stream.of("aaa", "bbb", "ccc", "ddd");
//重點(diǎn)對(duì)比這一行,map返回的是Stream<Stream<Character>>,flatMap返回的是Stream<Character>
Stream<Character> characterStream = stream.flatMap(s -> toCharacterStream(s));
characterStream.forEach(System.out::println);
}
public static Stream<Character> toCharacterStream(String str) {
List<Character> list = new ArrayList<>();
for (char c : str.toCharArray()) {
list.add(c);
}
return list.stream();
}
你品,你細(xì)細(xì)的品?。?/p>
3.3 排序
3.3.1 自然排序
自然排序需要流中的實(shí)例實(shí)現(xiàn)了Comparable接口。
personList.stream().sorted().forEach(System.out::println);
3.3.2 定制排序
定制排序,需要傳入一個(gè)Comparator
personList.stream().sorted((o1, o2) -> o1.getAge()-o2.getAge()).forEach(System.out::println);
四. 終止操作
4.1 查找與匹配
- allMatch:檢查是否匹配所有元素,返回boolean
- anyMatch:檢查是否至少匹配一個(gè)元素,返回boolean
- noneMatch:檢查是否沒有匹配所有元素,返回boolean
- findFirst:返回第一個(gè)元素
- findAny:返回當(dāng)前流中任意元素
- count:返回元素中元素的總個(gè)數(shù)
- max:返回流中的最大值
- min:返回流中的最小值
- forEach:循環(huán)
4.2 規(guī)約(reduce)
reduce是將流中的元素反復(fù)結(jié)合起來,得到一個(gè)最終值:
//沒有初始值的規(guī)約
Optional<T> reduce(BinaryOperator<T> accumulator);
//identity是初始值,accumulator是一個(gè)二元運(yùn)算
T reduce(T identity, BinaryOperator<T> accumulator);
例一
數(shù)組中所有元素的求和:
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Integer sum = stream.reduce(0, (integer, integer2) -> integer + integer2);
//Integer sum = stream.reduce(0, Integer::sum); 與上面具有想用意義
System.out.println(sum);
例二
計(jì)算所有人的年齡總和:
Optional<Integer> reduce = personList.stream().map(person -> person.getAge()).reduce(Integer::sum);
System.out.println(reduce.get());
4.3 收集(collect)
collect()將流轉(zhuǎn)化為其他形式。接收一個(gè)Collector(收集器)接口的實(shí)現(xiàn),用于收集流中的元素。Collector接口中的方法決定了如何對(duì)流進(jìn)行收集操作,Collectors類提供了很多靜態(tài)方法,可以方便地創(chuàng)建常用的收集器:
4.3.1 收集為數(shù)組
Person[] personArray = personList.stream().toArray(Person[]::new);
4.3.2 收集成集合
Collectors.toList():將所有元素收集到一個(gè)List中
List<Person> persons = personList.stream().collect(Collectors.toList());
Collectors.toSet():將所有元素收集到一個(gè)Set中
Set<Person> persons = personList.stream().collect(Collectors.toSet());
Collectors.toMap():將所有元素收集到一個(gè)Map中
//將Person實(shí)例的name屬性作為Key,person對(duì)象作為value放入Map中
Map<String, Person> collect = personList.stream().distinct().collect(Collectors.toMap(person -> person.getName(),o -> o));
Collectors.toCollection():將所有元素放入集合
HashSet<Person> collect = personList.stream().collect(Collectors.toCollection(HashSet::new));
4.3.3 收集聚合信息(類似于SQL中的聚合函數(shù))
Collectors.averagingInt():收集所有元素求平均值。
//獲取所有Person的平均年齡
Double average = personList.stream().collect(Collectors.averagingInt(Person::getAge));
Collectors.counting():計(jì)算元素總數(shù)量
Long count = personList.stream().collect(Collectors.counting());
Collectors.summing():計(jì)算元素總和
//求出年齡的總和
Integer sum = personList.stream().collect(Collectors.summingInt(Person::getAge));
Collectors.maxBy():最大值(與max()方法效果一樣)Collectors.minBy():最小值
//求出年齡最大的人
Optional<Person> max = personList.stream().collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge()));
Collectors.summarizingInt:可以獲取所有的聚合信息。
//獲取年齡的所有聚合信息
IntSummaryStatistics summary = personList.stream().collect(Collectors.summarizingInt(Person::getAge));
System.out.println(summary.getMax());
System.out.println(summary.getAverage());
System.out.println(summary.getCount());
System.out.println(summary.getMin());
4.3.4 分組收集(類似于SQL中的group by語句)
//分類器函數(shù)將輸入元素映射到鍵(單級(jí)分組)
groupingBy(Function<? super T, ? extends K> classifier)
//多級(jí)分組。downstream實(shí)現(xiàn)下級(jí)分組
groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream)
//多級(jí)分組,并且指定當(dāng)前分組創(chuàng)建Map的方法
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream)
例如我們需要按照年齡進(jìn)行分組:
//按照年齡進(jìn)行分組
Map<Integer, List<Person>> collect = personList.stream().collect(Collectors.groupingBy(person -> person.getAge()));
Map<Integer, List<Person>> collect = personList.stream().collect(Collectors.groupingBy(Person::getAge, HashMap::new, Collectors.toCollection(ArrayList::new)));
{
18: [{
"age": 18,
"country": "中國",
"name": "歐陽雪",
"sex": "F"
}],
20: [{
"age": 20,
"country": "中國",
"name": "向天笑",
"sex": "M"
}, {
"age": 20,
"country": "中國",
"name": "小梅",
"sex": "F"
}],
...
}
多級(jí)分組
我們可以通過傳入下級(jí)分組器,來實(shí)現(xiàn)多級(jí)分組。例如我們需要先按照年齡進(jìn)行分組,然后再按照國籍進(jìn)行分組:
Map<Integer, Map<String, List<Person>>> collect = personList.stream().collect(
Collectors.groupingBy(person -> person.getAge(), Collectors.groupingBy(o -> o.getCountry())));
//或者這么寫也可以
Map<Integer, Map<String, List<Person>>> collect = personList.stream().collect(
Collectors.groupingBy(Person::getAge, Collectors.groupingBy(Person::getCountry)));
如果我們每一級(jí)的集合對(duì)象都需要自定義,我們可以這樣做:
HashMap<Integer, TreeMap<String, LinkedList<Person>>> collect = personList.stream().collect(
Collectors.groupingBy(Person::getAge,
HashMap::new, Collectors.groupingBy(Person::getCountry,TreeMap::new,
Collectors.toCollection(LinkedList::new))));
{
18: {
"中國": [{
"age": 18,
"country": "中國",
"name": "歐陽雪",
"sex": "F"
}]
},
22: {
"中國": [{
"age": 22,
"country": "中國",
"name": "李康",
"sex": "M"
}, {
"age": 22,
"country": "中國",
"name": "李康",
"sex": "M"
}],
"英國": [{
"age": 22,
"country": "英國",
"name": "Harley",
"sex": "F"
}]
}
}
4.3.5 分區(qū)收集
分區(qū)收集能將符合條件的放在一個(gè)集合中,不符合條件的放在另一個(gè)集合中
//將年齡大于等于20的分為一組,將年齡小于20的分為一組(實(shí)際上用groupBy也能實(shí)現(xiàn))
Map<Boolean, List<Person>> collect = personList.stream().collect(Collectors.partitioningBy(person -> person.getAge() >= 20));
4.3.6 拼接字符串
Collectors.joining收集器按順序?qū)⑤斎朐剡B接到一個(gè)字符串中:
//將姓名拼成字符串中間用逗號(hào)隔開,首尾用大括號(hào)括起來
String str = personList.stream().map(Person::getName).collect(Collectors.joining(",","{","}"));
{歐陽雪,Tom,Harley,向天笑,李康,小梅,何雪,李康}
4.4 循環(huán)
- foreach
personList.stream().forEach(System.out::println);
五. 并行流
前面我么所將的流的操作都建立在串行流的基礎(chǔ)上,在數(shù)據(jù)量小的情況下沒有任何問題,但是一旦數(shù)據(jù)量多起來,單線程的效率問題就會(huì)凸顯。
不同于串行流,并行流底層使用了Fork-Join框架,將任務(wù)分派到多個(gè)線程上執(zhí)行,這樣可以大大提高CPU資源的利用率。
5.1 如何創(chuàng)建并行流
在創(chuàng)建時(shí)直接創(chuàng)建并行流:
Collection實(shí)例.parallelStream();
將串行流轉(zhuǎn)化為并行流:
Stream實(shí)例.parallel();
將并行流轉(zhuǎn)化為串行流:
Stream實(shí)例.sequential();
5.2 串行流和并行流的對(duì)比
我們分別使用串行流和并行流進(jìn)行一百億個(gè)數(shù)的累加:
串行流:
long start = System.currentTimeMillis();
LongStream stream = LongStream.rangeClosed(0, 10000000000L);
long reduce = stream.reduce(0, (left, right) -> left + right);
long end = System.currentTimeMillis();
System.out.println("付費(fèi):" + (end - start) );
通過測(cè)試我們發(fā)現(xiàn),串行流需要55s,通過任務(wù)管理器也能發(fā)現(xiàn)CPU資源并沒有充分利用。

并行流:
long start = System.currentTimeMillis();
LongStream stream = LongStream.rangeClosed(0, 100000000000L);
long reduce = stream.parallel().reduce(0, (left, right) -> left + right);
long end = System.currentTimeMillis();
System.out.println("付費(fèi):" + (end - start) );
并行流花費(fèi):20S,并行流在執(zhí)行時(shí)能夠充分利用CPU資源。


浙公網(wǎng)安備 33010602011771號(hào)