Stream流
Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
什么是 Stream?
Stream(流)是一个来自数据源的元素队列并支持聚合操作
元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
数据源 流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。
和以前的Collection操作不同, Stream操作还有两个基础的特征:
Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
生成流
在 Java 8 中, 集合接口有两个方法来生成流:
stream() − 为集合创建串行流。
parallelStream() − 为集合创建并行流。
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
如果你有一个数组,可以使用静态的Stream.of方法
Stream<String> song=Stream.of("gently","down","the","stream");
中间操作
对于数据流来说,中间操作符在执行制定处理程序后,产生的结果仍然是一个数据流。
需要注意的是流操作不改变底层元素
中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):
map(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。
flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。
limit 限流操作,比如数据流中有10个 我只要出前3个就可以使用。
distint 去重操作,对重复元素去重,底层使用了equals方法。
filter 过滤操作,把不想要的数据过滤。
peek 挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。
skip 跳过操作,跳过某些元素。
sorted(unordered) 排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。
例子:
Stream.of("apple","banana","orange").map(e ->e.length()).forEach(e ->System.out.println(e));
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
List<Integer> squaresList = numbers.stream()
.map(i -> i * i)
.sorted((x, y) -> {return y - x})
.collect(Collectors.toList());
User w = new User("w",10);
User x = new User("x",11);
User y = new User("y",12);
Stream.of(w,x,y)
.peek(e->{e.setName(e.getAge()+e.getName());}) //重新设置名字 变成 年龄+名字
.forEach(e->System.out.println(e.toString()));
User x = new User("x",11);
User y = new User("y",12);
User w = new User("w",10);
Stream.of(w,x,y)
.sorted((e1,e2)->e1.age>e2.age?1:e1.age==e2.age?0:-1)
.forEach(e->System.out.println(e.toString()));
终止操作符
数据经过中间加工操作,就轮到终止操作符上场了;终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。
collect 收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。
count 统计操作,统计最终的数据个数。
findFirst、findAny 查找操作,查找第一个、查找任何一个 返回的类型为Optional。
noneMatch、allMatch、anyMatch 匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。
min、max 最值操作,需要自定义比较器,返回数据流中最大最小的值。
reduce 规约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。
forEach、forEachOrdered 遍历操作,这里就是对最终的数据进行消费了。
toArray 数组操作,将数据流的元素转换成数组。
collect
需要注意的是collect方法,官方提供的Collectors有很多API,
数据收集:set、map、list
聚合归约:统计、求和、最值、平均、字符串拼接、规约
前后处理:分区、分组、自定义操作
接下来讲几个比较重要的
1.Collectors.toCollection() 将数据转成Collection,只要是Collection 的实现都可以,例如ArrayList、HashSet ,该方法接受一个Supplier函数式接口(相当于创建对象的工厂,一般为 类名 :: new)。
//List
Stream.of(1,2,3,4,5,6,8,9,0)
.collect(Collectors.toCollection(ArrayList :: new ));
//Set
Stream.of(1,2,3,4,5,6,8,9,0)
.collect(Collectors.toCollection(HashSet :: new));
2.Collectors.toList()和Collectors.toSet() 其实和Collectors.toCollection() 差不多,只是指定了容器的类型,默认使用ArrayList 和 HashSet。本来我以为这两个方法的内部会使用到Collectors.toCollection(),结果并不是,而是在内部new了一个CollectorImpl。
//List
Stream.of(1,2,3,4,5,6,8,9,0)
.collect(Collectors.toList());
//Set
Stream.of(1,2,3,4,5,6,8,9,0)
.collect(Collectors.toSet());
3.Collectors.toMap() 和Collectors.toConcurrentMap(),见名知义,收集成Map和ConcurrentMap,默认使用HashMap和ConcurrentHashMap。这里toConcurrentMap()是可以支持并行收集的,这两种类型都有三个重载方法,不管是Map 还是ConcurrentMap,他们和Collection的区别是Map 是K-V 形式的,所以在收集成Map的时候必须指定收集的K(依据)。这里toMap()和toConcurrentMap() 最少参数是,key的获取,要存的value。
Collectors.toMap 有三个重载方法:
//Function表明该参数是一个lamba表达式,作用是进行类型转换
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper);
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction);
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier);
示例:这里以Student 这个结构为例,Student 包含 id、name。
public class Student{
//唯一
private String id;
private String name;
public Student() {
}
public Student(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Student studentA = new Student("20190001","小明");
Student studentB = new Student("20190002","小红");
Student studentC = new Student("20190003","小丁");
//串行收集
Stream.of(studentA,studentB,studentC)
.collect(Collectors.toMap(s -> s.getName(),s -> s));
//并发收集
Stream.of(studentA,studentB,studentC)
.parallel()
.collect(Collectors.toConcurrentMap(s -> s.getName(),s -> s));
那么如果key重复的该怎么处理?这里我们假设有两个id相同Student,如果他们id相同,在转成Map的时候,取name大一个,小的将会被丢弃。
Stream.of(studentA, studentB, studentC)
.collect(Collectors
.toMap(s -> s.getName(),s -> s,
(s1, s2) -> {
//这里使用compareTo 方法 s1>s2 会返回1,s1==s2 返回0 ,否则返回-1
if (((Student) s1).name.compareTo(((Student) s2).name) < -1) {
return s2;
} else {
return s1;
}
}));
4.Collectors.joining(),拼接,有三个重载方法,底层实现是StringBuilder,通过append方法拼接到一起,并且可以自定义分隔符(这个感觉还是很有用的,很多时候需要把一个list转成一个String,指定分隔符就可以实现了,非常方便)、前缀、后缀。
Student studentA = new Student("20190001", "小明");
Student studentB = new Student("20190002", "小红");
Student studentC = new Student("20190003", "小丁");
//使用分隔符:201900012019000220190003
Stream.of(studentA, studentB, studentC)
.map(Student::getId)
.collect(Collectors.joining());
//使用^_^ 作为分隔符
//20190001^_^20190002^_^20190003
Stream.of(studentA, studentB, studentC)
.map(Student::getId)
.collect(Collectors.joining("^_^"));
//使用^_^ 作为分隔符
//[]作为前后缀
//[20190001^_^20190002^_^20190003]
Stream.of(studentA, studentB, studentC)
.map(Student::getId)
.collect(Collectors.joining("^_^", "[", "]"));
5.Collectors.groupingBy()和Collectors.groupingByConcurrent(),这两者区别也仅是单线程和多线程的使用场景。为什么要groupingBy归类为前后处理呢?groupingBy 是在数据收集前分组的,再将分好组的数据传递给下游的收集器。
这是 groupingBy最长的参数的函数classifier 是分类器,mapFactory map的工厂,downstream下游的收集器,正是downstream 的存在,可以在数据传递个下游之前做很多的骚操作。
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream)
示例:这里将一组数整型数分为正数、负数、零,groupingByConcurrent()的参数也是跟它一样的就不举例了。
//Map<String,List<Integer>>
Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
.collect(Collectors.groupingBy(integer -> {
if (integer < 0) {
return "小于";
} else if (integer == 0) {
return "等于";
} else {
return "大于";
}
}));
//Map<String,Set<Integer>>
//自定义下游收集器
Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
.collect(Collectors.groupingBy(integer -> {
if (integer < 0) {
return "小于";
} else if (integer == 0) {
return "等于";
} else {
return "大于";
}
},Collectors.toSet()));
//Map<String,Set<Integer>>
//自定义map容器 和 下游收集器
Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
.collect(Collectors.groupingBy(integer -> {
if (integer < 0) {
return "小于";
} else if (integer == 0) {
return "等于";
} else {
return "大于";
}
},new LinkedHashMap(),Collectors.toSet()));
//Map<String ,Integer>
//统计每一组元素的个数
Stream.of(-6, -7, -8, -9, 1, 2, 3, 4, 5, 6)
.collect(Collectors.groupingBy(integer -> {
if (integer < 0) {
return "小于";
} else if (integer == 0) {
return "等于";
} else {
return "大于";
}
},Collectors.counting());
reduce
我们先考虑一下min()和max(),其中min()是返回流中的最小值,而max()返回流中最大值,前提是他们存在。他们之间的特点是什么?①都返回了一个值②由一可知,他们是终端操作。如果我们用流API的术语来形容前面这两种特性的结合体的话,它们代表了缩减操作。因为每个缩减操作都把一个流缩减为一个值,好比最大值,最小值。当然流API,把min()和max(),count()这些操作称为特例缩减。即然说到了特例,肯定就有泛化这种概念了,他就是reduce()方法了。
public interface Stream<T> extends BaseStream<T, Stream<T>> {
//、、、忽略其他无关紧要的元素
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
}
Stream接口定义了三个版本的reduce(),第一个版本返回的是一个T类型的对象,T代表的是流中的元素类型!第二个版本是返回一个Optional类型对象。对于这两种形式,accumulator是一个操作两个值并得到结果的函数。在第一个版本当中,identity是这样一个值,对于涉及identity和流中任意的累积操作,得到的结果就是元素自身,没有任何改变。比如,如果是加法,他就是0,如果是乘法他就是1。
其中的accumulator是一个BinaryOperator
第三个版本多用于并行流中,其中combiner是一个合并器,用来将不同流的结果合并,参数同accumulator
例子:
List<Integer> lists = new ArrayList<>();
lists.add(1);
lists.add(2);
lists.add(3);
lists.add(4);
lists.add(5);
lists.add(6);
Integer sum = lists.stream().reduce(0, (a, b) -> a + b);//21
System.out.println("list的总和为:" + sum);
Integer product = lists.stream().reduce(1, (a, b) -> a * b);
System.out.println("list的积为:" + product);//720
Integer product2 = lists.stream().reduce(1, (a, b) -> {
if (b % 2 == 0) return a * b; else return a; //偶数相乘
});
System.out.println("list的偶数的积为:" + product2);//48
并行Stream
可以使用集合接口的parallelStream()方法创建并行流 或者 使用流的parallel()方法转化为并行流
需要注意:
如果集合中或者数组中的元素是有序的,那么对应的流也是有序的。但是在使用并行流时,有时候流是无序的就能获得性能上的提升。因为如果流是无序的,那么流的每个部分都可以被单独的操作,而不需要与其他部分协调,从而提升性能。(又是无状态,说好的退休了呢)。所以当流操作的顺序不重要的时候,可以通过BaseStream接口提供的unordered()方法把流转换成一个无序流之后,再进行各种操作。
另外一点:forEach()方法不一定会保留并行流的顺序,如果在对并行流的每个元素执行操作时,也希望保留顺序,那么可以使用forEachOrdered()方法,它的用法和forEach()是一样的。

浙公网安备 33010602011771号