开心一刻
今天和朋友们去K歌,看着这群年轻人一个个唱的贼嗨,不禁感慨道:年轻真好啊!
想到自己年轻的时候,那也是拿着麦克风不放的人
现在的我没那激情了,只喜欢坐在角落里,默默的听着他们唱,就连旁边的妹子都劝我说:大哥别摸了,唱首歌吧
Stream 初体验
很多时候,我们往往会选择在数据库层面进行数据的过滤、汇聚,这就导致我们对 JDK8 的 Stream 应用的特别少,对它也就特别陌生了
但有时候,我们可以将原始数据加载到内存,在内存中进行数据的过滤和汇聚,这样可以减少数据库操作,提高查询效率(非绝对,数据量不大或走索引的情况下,数据库查询也是很快的)
假设我们在内存中进行数据的过滤、汇聚,在 JDK8 之前(或不用 JDK8 的 Stream),我们会如何处理? 多次 for 循环结合 if ,并创建多个集合来存放中间结果,最后对中间结果进行汇聚,代码量会非常大;如果想牛逼一点,用多线程来处理,那就更复杂了(线程池、并发等问题)。Stream 就解决了这些痛点,如果你的 JDK 版本是 8(或更高),你还在用 for 循环进行数据的过滤和汇聚,那就有点这味了
那 Stream 到底是何方神圣,让楼主如此推崇,我们往下看(再不讲重点,楼主怕是要收刀片了!)
先闻其声
我们先来看看她妈是怎么介绍她的: A sequence of elements supporting sequential and parallel aggregate operations.
我们能从中获取到两个信息:
1、Stream 是元素的集合(有点类似 Iterator)
2、对原 Stream 支持顺序或并行的汇聚操作
这她妈的介绍还是比较抽象,我们需要从 Stream 自身下手,慢慢去了解她
常见的 Stream 接口继承关系如下
IntStream, LongStream, DoubleStream 对应的是三种基本类型(int, long, double,不是包装类型),Stream 对应所有剩余类型
为什么不是这样
或者取消掉 IntStream, LongStream, DoubleStream,由 Stream 对应所有类型 ?
我们知道基本类型与包装类型之前的装箱与拆箱是有性能消耗的,频繁的转换会有比较严重的性能损耗,所以为不同数据类型设置不同stream接口,可以提高性能,也可以增加特定接口
一睹芳容
上面说了那么多,却始终未一睹 Stream 的芳容,心里着急呀!我们先来瞟一眼
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 8, 0, 5, 3, 2);
// 统计大于 3 的元素个数
long count = nums.stream().filter(e -> e > 3).count();
是不是很美?千万不要以为 Stream 就这?这还只是她的一条腿,她浑身上下都是宝
通过上面的简单示例,我们可以剖析出 Stream 的通用语法
也就是说使用 Stream 基本分三步:创建 Steam、转换Stream、汇聚,下面我们就从这三步详细介绍 Stream
创建 Stream
Stream 的创建方式有很多,我们只讲最常用的两种
基于数组: Stream<String> arrayStream = Arrays.stream(new String[]{"123", "abc", "A", "张三"});
基于 Collection: Stream<String> collectionStream = Arrays.asList("123", "abc", "A", "张三").stream();
把数组变成 Stream 使用 Arrays.stream() 方法;对于 Collection(List、Set、Queue 等),直接调用 stream() 方法就可以获得 Stream
转换 Stream
转换 Stream 的目的是对原 Stream 做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用,对原 Stream 是没有任何影响的。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历
由于获取的是一个新的流,而不是我们需要的最终结果,所以 转换 Stream 这个操作有个官方的称呼: Intermediate ,即中间操作
具体的转换操作有很多,我们挑一些常用的来说明一下
distinct
对 Stream 中的元素进行去重操作(去重逻辑依赖元素的 equals 方法),新生成的 Stream 中没有重复的元素
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> distinctStream = nums.stream().distinct();
filter
对 Stream 中的每个元素使用给定的过滤条件进行过滤操作,新生成的 Stream 只包含符合条件的元素
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> filterStream = nums.stream().filter(e -> e >= 2);
map
对 Stream 中的每个元素按给定的转换规则进行转换操作,新生成的 Stream 只包含转换生成的元素
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<String> mapStream = nums.stream().map(e -> e * e "");
JDK1.8 还提供了三个专门针对基本数据类型的 map 变种方法:mapToInt,mapToLong 和 mapToDouble。这三个方法也比较好理解,就是把原始 Stream 转换成一个新的 Stream,这个新生成的 Stream 中的元素都是对应的基本类型。之所以会有这三个变种方法,是考虑到自动装箱/拆箱的额外消耗
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
IntStream intStream = nums.stream().mapToInt(e -> e * 2);
LongStream longStream = nums.stream().mapToLong(e -> 3L * e);
DoubleStream doubleStream = nums.stream().mapToDouble(e -> 3.0 * e);
flatMap
与 map 类似
代码语言:javascript复制<R> Stream<R> map(Function<? super T, ? extends R> mapper);
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
不同的是 flatMap 中每个元素转换得到的是 Stream 对象,然后会把子 Stream 中的元素都放到新的 Stream 中
代码语言:javascript复制List<List<String>> groupList = Arrays.asList(Arrays.asList("q","w","e"), Arrays.asList("a", "s", "d"), Arrays.asList("z","x", "c"));
Stream<String> superStarStream = groupList.stream().flatMap(group -> group.stream().map(e -> e 1));
简单点理解就是:把几个小的集合中的元素经过处理后合并到一个大的集合中
类似的,JDK1.8 也提供了三个专门针对基本数据类型的 flatMap 变种方法:flatMapToInt,flatMapToLong 和 flatMapToDouble
limit
拷贝原 Stream 中的前 N 个元素到新的 Stream 中,如果原 Stream 中包含的元素个数小于 N,那就获取其所有的元素
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> limitStream = nums.stream().limit(4);
skip
拷贝原 Stream 除了前 N 个元素后剩下的所有元素到新 Stream,如果原 Stream 中包含的元素个数小于 N,那么返回空 Stream
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Stream<Integer> skipStream = nums.stream().skip(4);
sorted
对原 Stream 进行排序操作,得到一个新的、有序的 Stream
排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序
代码语言:javascript复制Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
使用起来非常简单,如下所示
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
// 自然排序,默认升序排序
Stream<Integer> sortedStream = nums.stream().sorted();
// 自定义排序
Stream<Integer> sortedCompareStream = nums.stream().sorted((a, b) -> b.compareTo(a));
peek
代码语言:javascript复制Stream<T> peek(Consumer<? super T> action);
生成一个包含原 Stream 所有元素的新 Stream,同时会提供一个消费函数(Consumer 实例),新 Stream 每个元素被消费的时候都会执行给定的消费函数
与 map 很像,但不会影响新 Stream 中的元素(还是原 Stream 中的元素),可以做一些输出,外部处理等辅助操作
这个在实际项目中用的不多,知道是怎么回事就好
汇聚
汇聚操作接受一个 Stream 为输入,反复使用某个汇聚操作,把 Stream 中的元素合并成一个汇总的结果,汇总结果可能是某个值,也可能是一个集合
汇聚操作能够得到我们需要的最终结果,相当于一个终止操作,所以也有另一个称呼: Terminal ,即结束操作
一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历
JDK1.8 提供了很多常用的汇聚操作,我们一起来看看
foreach
这个类似我们平时的 for 循环,遍历 Stream 中的元素,执行指定的操作
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
nums.stream().forEach(num -> System.out.println(num));
max min count
作用就是字面意思
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
// 求最大值
Integer max = nums.stream().max(Comparator.naturalOrder()).get();
// 求最小值
Integer min = nums.stream().min(Comparator.naturalOrder()).get();
// 求元素个数
long count = nums.stream().count();
System.out.println("max = " max);
System.out.println("min = " min);
System.out.println("count = " count);
findFirst
返回一个 Optional,它包含了 Stream 中的第一个元素,若 Stream 是空的,则返回一个空的 Optional
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Integer firstNum = nums.stream().findFirst().get();
findAny
返回一个 Optional,它包含了 Stream 中的任意一个元素,若 Stream 是空的,则返回一个空的 Optional
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
Integer anyNum = nums.stream().findAny().get();
在串行的流中,findAny 和 findFirst返回的,都是第一个对象;而在并行的流中,findAny 返回的是最快处理完的那个线程的数据,所以说,在并行操作中,对数据没有顺序上的要求,那么 findAny 的效率会比 findFirst 要快的,但是没有 findFirst 稳定
anyMatch
Stream 中是否有任意一个元素满足判断条件,有则返回 true
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
boolean matchResult = nums.stream().anyMatch(num -> num > 2);
allMatch
Stream 中所有元素都满足判断条件则返回 true
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
boolean matchResult = nums.stream().allMatch(num -> num > 2);
noneMatch
与 allMatch 相反,Stream 中所有元素都不满足判断条件,则返回 true
代码语言:javascript复制List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
boolean matchResult = nums.stream().noneMatch(num -> num > 7);
reduce
reduce 的主要作用是把 Stream 元素组合起来
它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和 Stream 中的第一个、第二个、第 n 个元素组合,生成一个我们需要的值
JDK 提供了三种 reduce
代码语言:javascript复制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);
参数不同,其返回值类型是有所不同的,但其语义、作用还是一样的
max()、min()其实都是特殊的 reduce,只是因为它们比较常用,所以就简化书写专门设计出了它们
代码语言:javascript复制@Override
public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
return reduce(BinaryOperator.maxBy(comparator));
}
@Override
public final Optional<P_OUT> min(Comparator<? super P_OUT> comparator) {
return reduce(BinaryOperator.minBy(comparator));
}
reduce 在实际项目中用的不多,又非常灵活,我们就简单看几个示例
代码语言:javascript复制 List<Integer> nums = Arrays.asList(1, 2, 1, 3, 2, 5);
// 求和,相当于sum(); 有起始值
Integer sum1 = nums.stream().reduce(0, Integer::sum);
Integer sum2 = nums.stream().reduce(0, (a,b) -> a b);
// 求和,相当于sum(); 无起始值
Integer sum3 = nums.stream().reduce(Integer::sum).get();
System.out.println("sum = " sum1 ", sum2 = " sum2 ", sum3 = " sum3);
// 求最大值,相当于max()
Integer max = nums.stream().reduce(Integer.MIN_VALUE, Integer::max);
System.out.println("max = " max);
// 求最小值,相当于min()
Integer min = nums.stream().reduce(Integer.MAX_VALUE, Integer::min);
System.out.println("min = " min);
reduce 擅长的是生成一个值,如果想要从 Stream 生成一个集合或者 Map 等复杂的对象该怎么办呢?就需要 collect 出马了
collect
collect 是 Stream 接口中最灵活的,也是最强大的;JDK 中提供了两种 collect
代码语言:javascript复制// Supplier supplier是一个工厂函数,用来生成一个新的容器;
// BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中
// BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
我们来各看一个案例
代码语言:javascript复制List<String> strList = Arrays.asList("123","abc", "1w1");
String concat = strList.stream().collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
System.out.println(concat);
List<String> stringList = strList.stream().collect(Collectors.toList());
实际应用中,基本上用的是第二种,而且用的是 JDK 中已经提供好的 Collector,在 Collectors 中提供了很多常用的 Collector, 如下
我们挑一些比较常用的来说明下,有兴趣的可以去通读下
转集合
toList、toSet、toMap
代码语言:javascript复制public class StreamTest {
public static void main(String[] args) {
Person[] personArray = {
new Person("shangsan", 23), new Person("张三", 23),
new Person("lisi", 24), new Person("李四", 24),
new Person("wangwu", 20), new Person("王五", 20)};
// 转 list
List<Person> personList = Arrays.stream(personArray).collect(Collectors.toList());
// 转 set
Set<Person> personSet = Arrays.stream(personArray).collect(Collectors.toSet());
// 转 map, key为 name, value 为 Person 实例
Map<String, Person> personMap = Arrays.stream(personArray).collect(Collectors.toMap(Person::getName, person -> person));
System.out.println(personList);
System.out.println(personSet);
System.out.println(personMap);
}
static class Person {
private String name;
private Integer age;
Person(){}
Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{"
"name='" name '''
", age=" age
'}';
}
}
}
toMap 有两个注意点
1、底层调用的是 map.merge 方法,该方法遇到 value 为 null 的情况会报 npe
2、遇到重复的 key 会直接抛 IllegalStateException,因为未指定冲突合并策略,也就是第三个参数BinaryOperator<U> mergeFunction
分组
代码语言:javascript复制public class StreamTest {
public static void main(String[] args) {
Person[] personArray = {
new Person("shangsan", 23), new Person("张三", 23),
new Person("lisi", 24), new Person("李四", 24),
new Person("wangwu", 20), new Person("王五", 20)};
// 根据年龄进行分组
Map<Integer, List<Person>> ageGroup = Arrays.stream(personArray).collect(Collectors.groupingBy(Person::getAge));
System.out.println(ageGroup);
}
static class Person {
private String name;
private Integer age;
Person(){}
Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Person{"
"name='" name '''
", age=" age
'}';
}
}
}
求和
代码语言:javascript复制// 年龄求和 summingInt、summingLong、summingDouble 类似
Integer ageSum = Arrays.stream(personArray).collect(Collectors.summingInt(Person::getAge));
System.out.println("age sum = " ageSum);
求平均值
代码语言:javascript复制// 求平均值 averagingInt、averagingLong、averagingDouble 类似
Double averageAge = Arrays.stream(personArray).collect(Collectors.averagingInt(Person::getAge));
System.out.println("average age = " averageAge);
其他
代码语言:javascript复制// 统计人数
Long count = Arrays.stream(personArray).collect(Collectors.counting());
System.out.println("人数 = " count);
List<Integer> intList = Arrays.asList(1, 2, 3, 1, 5, 2);
// 求和
Integer sum = intList.stream().collect(Collectors.reducing(0, (a, b) -> a b));
System.out.println("sum = " sum);
// 字符串拼接
List<String> strList = Arrays.asList("123", "abc", "666");
String str = strList.stream().collect(Collectors.joining(",", "(", ")"));
System.out.println(str);
并行流
前面讲了那么多,都是基于顺序流(Stream),JDK1.8 也提供了并行流: parallelStream ,使用起来非常简单,通过 parallelStream() 可能创建并行流,流的操作还是和顺序流一样
代码语言:javascript复制List<Integer> intList = Arrays.asList(1, 2, 3, 1, 5, 2);
boolean result = intList.parallelStream().anyMatch(e -> e > 5);
System.out.println("result = " result);
顾名思义,并行流可以运用多核特性(forkAndJoin)进行并行处理,从而大幅提高效率,既然能提高效率,为什么实际项目中,顺序流用的更多,而并行流用的非常少了,还是有一些原因的
1、parallelStream 是线程不安全的
一旦出现并发问题,大家都懂的,非常头疼
2、parallelStream 适用于 CPU 密集型任务
如果 CPU 负载已经很大,还用并行流,不但不会提高效率,反而会降低效率
并行流不适用于 I/O 密集型任务,很可能会造成 I/O 阻塞
3、并行流无法保证元素顺序,输出结果具有不确定性
如果我们的业务需要关注元素先后顺序,那么不能用并行流
4、lambda 的执行并不是瞬间完成的,所有使用 parallel stream 的程序都有可能成为阻塞程序的源头
总结
Stream 特点
无存储:Stream 不是数据结构并不保存数据,它是有关算法和计算的,它只是某种数据源的一个视图,数据源可以是一个数组,Java 容器或 I/O channel
函数式编程:每次转换,原有 Stream 不改变,返回一个新的 Stream 对象,这就允许对其操作可以像链条一样排列;转换过程可以多次
惰性执行:Stream 上的转换操作(中间操作)并不会立即执行,只有执行汇聚操作(终止操作)时,转换操作才会执行
一次消费:Stream 只能被使用一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成
Stream 优点
代码简洁且易理解,这个感受是最明显的,用与不用 Stream,代码量与可阅读性相差甚远
多核友好,如果想多线程处理,只需要调一下 parallel() 方法,仅此而已
Stream 操作分类
分两类:中间操作(Intermediate)、结束操作(Terminal)
中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新 stream,仅此而已
结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以 pipeline 的方式执行,这样可以减少迭代次数;计算完成之后stream就会失效
性能问题
在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数
关于顺序流、并行流、传统 for 的效率问题,大家看看这个:Java Stream API性能测试、for循环与串行化、并行化Stream流性能对比
参考
Java 8 中的 Streams API 详解
JDK8函数式编程之Stream API