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 distinct();

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

filter()

Stream filter(Predicate predicate);

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

map()

     Stream map(Function mapper);

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

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

flatMap()

 Stream flatMap(Function> mapper);

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

peek()

Stream peek(Consumer action);

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

skip()

Stream 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 predicate)
public boolean  anyMatch(Predicate predicate)
public boolean  noneMatch(Predicate 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 reduce(BinaryOperator accumulator)
T reduce(T identity, BinaryOperator accumulator)
 U reduce(U identity, BiFunction accumulator, BinaryOperator 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 collect(Supplier supplier,
                 ObjIntConsumer accumulator,
                 BiConsumer combiner)

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

 R collect(Collector collector);

举个例子:

List list1 = Stream.of(1,2,3,4,5,6,7,8,9,10)
        .collect(() -> new ArrayList(),.//生成一个新的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 进行运算并很快完成。

你可能感兴趣的:(Java8:Stream详解)