1. 什么是流
流是从支持数据处理操作的源生的一系列元素。流利用内部迭代,中间操作会返回一个流并且可以链接在一起,可以用来设置一条流水线不生成任何结果。
流中的元素的按需计算的,类似于Python中的生成器,都是惰性计算,需要的时候产生结果。
个人认为有了流,操作起数据来方便程度跟python有得一拼。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19List<String> list = new ArrayList<>();
list.add("王麻子");
list.add("张三丰");
list.add("狮子");
//统计集合中有字符长度为3的元素个数
//普通写法
long count=0;
//外部迭代
for (String s : list) {
if(s.length()==3){
count++;
}
}
//使用流
long count = list.stream()
.filter((name) -> name.length() == 3)//内部迭代
.count();
首先第一步list.stream()支持流
第二步filter()过滤字符串长度为3的元素
第三部count()统计过滤后的元素个数
其中第二步称为中间操作
第三步称为终端操作
2. 中间操作
操作 | 操作类型 | 返回值 | 使用的类型/函数式接口 | 函数描述符 | 作用 |
---|---|---|---|---|---|
filter | 中间 | Stream<T> | Predicate<T> | T -> boolean | 过滤元素 |
map | 中间 | Stream<T> | Function<T, R> | T -> R | 映射元素 |
limit | 中间(有状态-有界) | Stream<T> | long | limit(n)返回前n个元素 | |
sorted | 中间(有状态-有界) | Stream<T> | Comparetor<T> | (T, T) -> int | 排序 |
distinct | 中间(有状态-无界) | Stream<T> | 去重 | ||
skip | 中间(有状态-有界) | Stream<T> | long | skip(n)跳过前n个元素 | |
flatMap | 中间 | Stream<T> | Function<T,Stream<R>> | T->Stream<R> |
3. 终端操作
操作 | 操作类型 | 返回值 | 使用的类型/函数式接口 | 函数描述符 | 作用 |
---|---|---|---|---|---|
forEach | 终端 | void | Consumer<T> | T->void | 消费流中的每个元素并对其应用Lambda。返回void |
count | 终端 | long | 返回流中元素的个数 | ||
collect | 终端 | R | Collector<T,A,T> | 把流归约成一个集合,比如List,Map甚至Integer。 | |
anyMatch | 终端 | boolean | Predicate<T> | T->boolean | 返回流中给匹配谓词的元素 |
findAny | 终端 | Optional |
返回任意一个元素 | ||
findFirst | 终端 | Optional |
返回第一个元素 | ||
reduce | 终端(有状态-有界) | Optional<T> | BinaryOperator | (T,T)->T | 归约 |
4. 筛选(filter)
Streams接口的filter会接收一个谓词(一个返回boolean的函数)作为参数,并且包含所有符合谓词表述的元素的流。
筛选出集合中的奇数,并且去重。1
2
3
4
5
6
7
8List<Integer> list = Arrays.asList(1,2,3,3,4,5,6,7,8,9,10);
list.stream()
.filter((i)->(i & 1)==1)
.distinct()
.forEach(System.out::println);
//.collect(toList()); //直接转换成集合返回
//终端操作只能一次
5. 映射(map)
例1:
统计每一个字符串的长度。1
2
3
4
5List<String> list = Arrays.asList("I", "am", "a", "Student");
list.stream()
.map(String::length)
.forEach(System.out::println);
例2:
将[“I”, “am”, “a”, “Student”]变成[[“I”, “a”, m”, “a”, “S”,”t”, “u”, “d”. “e”, “n”, “t”]]
解1
1 | List<String> list = Arrays.asList("I", "am", "a", "Student"); |
这样子处理返回的是List<String[]>类型的,是[[“I”],[“a”,”m”],[“a”],[…]], 跟预期的结果不一样。需要的是一个字符流,而不是数组流。
解2
如果让每个数组都变成单独的流呢?Arrays.stream()可以做到:1
2
3
4list.stream()
.map((word -> word.split("")))
.map(Arrays::stream) //让每个数组变成单独的流
.collect(toList());
但是这样做以后返回值会变成Stream
接下来就要用到新东西了,流的扁平化
6. 合并流(flatMap)
flatMap将各个生成流扁平化为单个流
解例2
1 | list.stream() |
终于达到了想要的结果
返回值为List
例3
给定[1,2,3]和[1,2]求他们的笛卡尔积。1
2
3
4
5
6List<Integer> num1 = Arrays.asList(1,2,3);
List<Integer> num2 = Arrays.asList(3,4);
List<int[]> collect = num1.stream()
.flatMap(i -> num2.stream()
.map(j -> new int[]{i, j})
).collect(toList());
7. 查找和匹配
7.1 匹配任意一个(anyMatch)
返回一个boolean,所以是一个终端操作。
1 | if(students.stream().anyMatch(Student::isGoodStudent)){ |
7.2 匹配所有(allMatch)
终端操作,跟anyMatch差不多,只有所有都为true才为true
7.3 没有任何匹配(noneMatch)
没有任何元素匹配才为true
anyMatch、allMatch、noneMatch都是短路操作。
7.4 查找任意一元素(findAny)
查找任意元素:可以结合filter过滤使用,会返回一个Optional[3]这样的东西,Optional
7.5 查找第一个元素(findFirst)
返回第一个元素。
8. 归约
把流中的元素全部组合起来,表达一个复杂的查询结果。比如计算总数,查找最大值。
8.1 reduce
reduce接收两个参数,有三个重载1
2
3
4
5
6
7
8//有初始值返回T
T reduce(T identity, BinaryOperator<T> accumulator);
//无初始值,返回Optional<T>
Optional<T> reduce(BinaryOperator<T> accumulator);
//???
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
这里的BinaryOperator
1 | //用第一个参数键入初始值,第二个参数将两个元素结合起来生成一个新值 |
用map和reduce统计集合中元素个数1
2
num1.stream().map(i->1).reduce((a,b)->a+b).get()
map-reduce模式容易并行化,google用这种模式做搜索。
8.2 reduce的优势
使用reduce的优势在于,迭代被内部迭代抽象了,这让内部实现可以选择并行执行reduce操作。如果使用外部迭代器,要不断更新共享变量sum,不容易并行化,如果使用同步synchronize和ReentrantLock之类的锁同步,需要将输入分块,分块求和,最后再合并起来。使用reduce只需要把stream()改成parallelStream()即可。
并行流(parallelStream())
并行流也可能会付出一些代价。
传递给reduce的Lambda不能更改状态(比如实例变量), 而且操作必须满足结合律才可以按任意顺序执行。
有状态和无状态
像map,reduce之类的操作会从输入流中获取每一个元素,并且在输出流中得到0/1个结果,这种操作叫无状态。
像reduce,sum,max操作需要内部状态积累结果,但是内部状态很小,不过内部状态是有界的。
像sort,distinct需要把元素都放入缓冲区后才能在输出流加入元素,如果流很大,或者是无限的就会有问题。
数值流
可以将流转换为数值,减少自动装箱、拆箱的消耗。
例:求需要发的员工工资总和。
每个Integer都要拆箱成一个int原始类型求和。1
2
3int totalSalary = empl.stream()
.map(Emploee::getSalary)
.reduce(0,Integer::sum);
java8用了三个原始类型特化流接口解决:IntStream、DoubleStream、LongStream,将对象流特化为对应的原始类型(数值流)。
映射到数值流
流转换方法:mapToInt、mapToDouble、mapToLong,跟原始的map工作方式一样,只不过返回的是特化流而不是Stream<T>。
1 | int totalSalary = empl.stream() |
转换回对象流
只需要使用boxed()装箱就行了。
1 | int totalSalary = empl.stream() |