Java8:Stream详解

文章目录

1. Stream概述?

JDK文档:

A sequence of elements supporting sequential and parallel aggregate operations.

中文翻译:

Stream是元素的集合,可以支持顺序和并行的对原Stream进行汇聚的操作;

Stream代表数据流,流中的数据元素的数量可能是有限的,也可能是无限的。

Java为什么要引入Stream?这个问题可以从侧面更好的了解Stream的概念

  • 通过函数式编程的方式可以将将复杂的数据处理过程变得简单明了,那么这个和Stream有什么关系?本质上Streams是Monads。

Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。

  • 提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势

Stream相关概念

  • 一系列元素:Stream对一组有特定类型的元素提供了一个接口。但是Stream并不真正存储元素,元素根据需求被计算出结果。
  • 源:Stream可以处理任何一种数据提供源,比如结合、数组,或者I/O资源。
  • 聚合操作:Stream支持类似SQL一样的操作,常规的操作都是函数式编程语言,比如filter,map,reduce,find,match,sorted,等等。

Stream操作还具备两个基本特性使它与集合操作不同:

  • 管道:许多Stream操作会返回一个stream对象本身。这就允许所有操作可以连接起来形成一个更大的管道。这就就可以进行特定的优化了,比如懒加载和短回路,我们将在下面介绍。
  • 内部迭代:和集合的显式迭代(外部迭代)相比,Stream操作不需要我们手动进行迭代。

总结Stream的特点

  • 不存储数据。流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
  • 函数式编程。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
  • 延迟操作。流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
  • 可以解绑。对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现”短路”(Short-circuiting),访问到有限的元素后就可以返回。
  • 纯消费。流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,需要重新生成一个新的流。

2. Stream的使用

2.1 流的操作类型

对stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations)

  • 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。
  • 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。

还有一种操作被称为 short-circuiting。用以指:
对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

2.2 使用Stream的步骤

创建Stream -> 转换Stream每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换) ->对Stream进行聚合(Reduce)操作,获取想要的结果

2.3 Stream创建
  1. 通过Collection的stream()方法或者parallelStream(),比如Arrays.asList(1,2,3).stream()。
  2. 使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator),如Stream.iterate(0, n -> n * 2),或者generate(Supplier s)如Stream.generate(Math::random)。
  3. BufferedReader.lines()从文件中获得行的流。
  4. Files类的操作路径的方法,如list、find、walk等。
  5. 随机数流Random.ints()
  6. 通过Arrays.stream(Object[])方法, 比如Arrays.stream(new int[]{1,2,3})。
  7. 其它一些类提供了创建流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()。

追踪到底层其实都是使用StreamSupport类,它提供了将Spliterator转换成流的方法。至于它的内部细节在下片文章介绍。

2.4 中间操作

中间操作会返回一个新的流,但是操作是延迟执行的(lazy),它不会修改原始的数据源,而且是由在终点操作开始的时候才真正开始执行。
这个Scala集合的转换操作不同,Scala集合转换操作会生成一个新的中间集合,显而易见Java的这种设计会减少中间对象的生成。

操作类型方法
中间操作concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()

区分中间操作和结束操作最简单的方法,就是看方法的返回值,返回值为stream的大都是中间操作,否则是结束操作。

distinct()

Stream<T> distinct();

对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;

filter()

Stream<T> filter(Predicate<? super T> predicate);

对于Stream中包含的元素使用给定的predicate过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素。

map()

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

对于Stream中包含的元素使用给定的转换函数进行转换操作,对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。

这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;

flatMap()

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;

peek()

Stream<T> peek(Consumer<? super T> action);

生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数

skip()

Stream<T> skip(long n);

返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

limit()
对一个Stream进行截断操作,获取其前N个元素。如果原Stream中包含的元素个数小于N,那就获取其所有的元素,这是一个short-circuiting 操作。

下面统一实例代码:

Arrays.asList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10)
       .stream()
       .distinct()
       .filter(t -> t != null && t > 9)
       .map(t -> t * 10)
       .flatMap(t -> Stream.of(t, t + 1))
       .skip(1)
       .forEach(System.out::println);
2.5 终结操作
操作类型方法
结束操作allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

Match

public boolean 	allMatch(Predicate<? super T> predicate)
public boolean 	anyMatch(Predicate<? super T> predicate)
public boolean 	noneMatch(Predicate<? super T> predicate)

这一组方法用来检查流中的元素是否满足断言。

  • allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true
  • anyMatch只有在任意一个元素满足断言时就返回true,否则flase,
  • noneMatch只有在所有的元素都不满足断言时才返回true,否则flase

count

count方法返回流中的元素的数量。它实现为:

forEach/forEachOrdered

forEach遍历流的每一个元素,执行指定的action。和peek方法不同。这个方法不担保按照流的encounter order顺序执行,如果对于有序流按照它的encounter order顺序执行,你可以使用forEachOrdered方法。

max/min

max返回流中的最大值,
min返回流中的最小值。

toArray()

将流中的元素放入到一个数组中。

总结:
终结操作也叫汇聚操作,它接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的汇聚操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。

注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有

汇聚操作可以分为以下两类

  • 可变汇聚:把输入的元素们累积到一个可变的容器中,如collect。

  • 其他汇聚:除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch。

collect和ruduce比较重要,单独一节讲述。

3. reduce

作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于

Integer sum = integers.reduce(0, (a, b) -> a+b); 
//或者
Integer sum = integers.reduce(0, Integer::sum);

下面看它的定义:

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

举个例子

Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .stream()
        .reduce((a, b) -> a + b)
        .ifPresent(System.out::println);
System.out.println(Stream.of("A", "B", "C", "D")
        .reduce("Str", (a, b) -> a + "-" + b));
Stream.of("I", "love", "you")
        .reduce(0,// 初始值 
                (sum, str) -> sum + str.length(),// 累加操作
                (a, b) -> 0); //并行stream才会用到

4. collect

可变汇聚对应的只有一个方法:collect,正如其名字显示的,它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。看一下它的定义:

 <R> R collect(Supplier<R> supplier,
                 ObjIntConsumer<R> accumulator,
                 BiConsumer<R, R> combiner)

先来看看这三个参数的含义: supplier是一个工厂函数,用来生成一个新的容器, accumulator用来把Stream中的元素添加到结果容器中,BiConsumer<R, R> combiner参数用来把中间状态的多个结果容器合并成为一个(并行的时候会用到)
还有一个重载函数,参数是Collector类型,三个参数太麻烦,收集器Collector就是对这三个参数的简单封装。Collectors工具类可通过静态方法生成各种常用的Collector。

<R, A> R collect(Collector<? super T, A, R> collector);

举个例子:

List list1 = Stream.of(1,2,3,4,5,6,7,8,9,10)
        .collect(() -> new ArrayList<Integer>(),.//生成一个新的ArrayList实例
       	 		//接受两个参数,第一个是前面生成的ArrayList对象,
         		//二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。
        		//此函数被反复调用直到原stream的元素被消费完毕;
                (list, item) -> list.add(item),
               //接受两个参数,这两个都是ArrayList类型的,函数体就是把第二个ArrayList全部加入到第一个中;
                (lista, listb) -> lista.addAll(listb));
List list2 = Stream.of(1,2,3,4,5,6,7,8,9,10)
        .collect(ArrayList::new,
                ArrayList::add,
                ArrayList::addAll);
List list3 =  Stream.of(1,2,3,4,5,6,7,8,9,10)
        .collect(Collectors.toList());
System.out.println(list3);

5. 总结

Stream的常用API基本介绍完毕,应该有了一个初步的认识。总结一下Stream 的特性:

  • 不是数据结构,它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。所以也不支持索引访问。

  • 所有 Stream 的操作必须以 lambda 表达式为参数

  • 惰性化,很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
    Intermediate 操作永远是惰性化的。

  • 并行能力,当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。

  • 可以是无限的,集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

    原文作者:TheLudlows
    原文地址: https://blog.csdn.net/TheLudlows/article/details/78782892
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞