1. 什么是流

流是从支持数据处理操作的源生的一系列元素。流利用内部迭代,中间操作会返回一个流并且可以链接在一起,可以用来设置一条流水线不生成任何结果。
流中的元素的按需计算的,类似于Python中的生成器,都是惰性计算,需要的时候产生结果。
个人认为有了流,操作起数据来方便程度跟python有得一拼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<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
8
List<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
5
List<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
2
3
4
List<String> list = Arrays.asList("I", "am", "a", "Student");
list.stream()
.map((word -> word.split("")))
.collect(toList());

这样子处理返回的是List<String[]>类型的,是[[“I”],[“a”,”m”],[“a”],[…]], 跟预期的结果不一样。需要的是一个字符流,而不是数组流。

解2

如果让每个数组都变成单独的流呢?Arrays.stream()可以做到:

1
2
3
4
list.stream()
.map((word -> word.split("")))
.map(Arrays::stream) //让每个数组变成单独的流
.collect(toList());

但是这样做以后返回值会变成Stream类型的,怎么合成一个流呢?

接下来就要用到新东西了,流的扁平化

6. 合并流(flatMap)

flatMap将各个生成流扁平化为单个流

解例2
1
2
3
4
list.stream()
.map((word -> word.split("")))
.flatMap(Arrays::stream)
.forEach(System.out::println);

终于达到了想要的结果
返回值为List

例3

给定[1,2,3]和[1,2]求他们的笛卡尔积。

1
2
3
4
5
6
List<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
2
3
if(students.stream().anyMatch(Student::isGoodStudent)){
System.out.println("有好孩子");
}

7.2 匹配所有(allMatch)

终端操作,跟anyMatch差不多,只有所有都为true才为true

7.3 没有任何匹配(noneMatch)

没有任何元素匹配才为true

anyMatch、allMatch、noneMatch都是短路操作。

7.4 查找任意一元素(findAny)

查找任意元素:可以结合filter过滤使用,会返回一个Optional[3]这样的东西,Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在。findAny可能什么都没找到,返回Optional[3]就可以不用返回null了。可以用T get()获取返回值(如果有的话,没有则返回NoSuchElem异常)。

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是继承自BiFunction的函数式接口

1
2
3
4
//用第一个参数键入初始值,第二个参数将两个元素结合起来生成一个新值
List<Integer> num1 = Arrays.asList(1,2,3,4,5,6);
int num = num1.stream().reduce(0,(a,b)->a+b);
System.out.println(num);//21

用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
3
int totalSalary = empl.stream()
.map(Emploee::getSalary)
.reduce(0,Integer::sum);

java8用了三个原始类型特化流接口解决:IntStream、DoubleStream、LongStream,将对象流特化为对应的原始类型(数值流)。

映射到数值流

流转换方法:mapToInt、mapToDouble、mapToLong,跟原始的map工作方式一样,只不过返回的是特化流而不是Stream<T>。

1
2
3
int totalSalary = empl.stream()
.mapToInt(Emploee::getSalary) //mapToInt返回值是IntStream,不是Stream<Integer>
.sum();

转换回对象流

只需要使用boxed()装箱就行了。

1
2
3
int totalSalary = empl.stream()
.map(Emploee::getSalary);
Stream<Integer> s = totalSalary.boxed();

扩展阅读

Java 8新特性(二):Stream API