java8的流以及流和集合的区别

    集合是一种内存中的数据结构,包含数据结构中目前所有的值,也就是说集合中的值都要先计算好才能够放入集合中,但是流则不同,流是概念上固定的数据结构其元素是按需计算的不能添加或者删除元素,只有在需要的时候才将需要的流计算出来。集合需要提前将值全部准备好而流则是将值准备一部分。

    集合和流的一个区别则是遍历数据的方式,使用Collection接口需要用户去进行迭代,也就是在集合的外部这称为外部迭代。而Streams则使用内部迭代,它会代替使用者在流的内部进行迭代,还会把流值存在某个地方,只需要通过函数进行操控就可以了,Streams的内部迭代还可以自动选择一种适合计算机硬件的数据表示和并行实现。同时流只能够遍历一次。可以仅仅是通过stream().filter().count()这三个函数完成遍历工作替代for循环。stream()操作将数据转换为流,filter()执行自定义的筛选处理,并进行计数。

    1.流并不存储其元素,这些元素可能存储在底层的集合中,或是按需生成。

    2.流的操作不会修改其数据源,而是会生成一个新的流。

    3.流的操作是惰性执行的,在需要其结果时才会执行,所以需要终止操作来产生结果,这个操作会强制执行之前的惰性操作。在使用终止操作之后这个流就不能在使用了。

    4.Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

    5.而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架来拆分任务和加速处理过程。

    6.Stream 的另外一大特点是,数据源本身可以是无限的。获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

     流允许以声明的形式处理数据集合。对流的定义是从支持数据处理操作的源生产的元素序列。也就是说Stream是对集合对象的功能加强,对其进行各种便利高效的聚合操作。将集合内的元素看成是一种流,流在管道内传输,并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。

    中间操作(intermediate)一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。主要有以下方法(此类型的方法返回的都是Stream对象): map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered。

    终端操作(terminal)一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。主要有以下方法: forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator。

    在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

     java8的新特性就是希望我们去表达我们想要完成什么而不是要怎样做。这同样是循环的不足之处因为要确保循环的灵活性是需要付出代价的。return、break 或者 continue都会显著地改变循环的实际表现。这迫使我们不仅要清楚我们要实现怎样的代码,还要了解循环是怎样工作的。java8的流式处理极大的简化了对于集合的操作,实际上不光是集合,包括数组、文件等,只要是可以转换成流,我们都可以借助流式处理,类似于我们写SQL语句一样对其进行操作。java8通过内部迭代来实现对流的处理,一个流式处理可以分为三个部分:转换成流、中间操作、终端操作。

    中间操作

    过滤:过滤,顾名思义就是按照给定的要求对集合进行筛选满足条件的元素,java8提供的筛选操作包括:filter、distinct、limit、skip。

    filter:其定义为:Stream<T> filter(Predicate<? super T> predicate),filter接受一个Predicate,我们可以通过它定义筛选条件。Predicate是一个函数式接口,其包含一个test(T t)方法,该方法返回boolean。

List<Student> whuStudents = students.stream().filter(student -> "xxxx".equals(student.getSchool())).collect(Collectors.toList());

    distinct:distinct操作类似于在写SQL语句时,添加的DISTINCT关键字用于去重处理,distinct基于Object.equals(Object)实现。假设我们希望筛选出所有不重复的偶数,那么可以添加distinct操作:

List<Integer> evens = nums.stream().filter(num -> num % 2 == 0).distinct().collect(Collectors.toList());

    limit:limit操作也类似于SQL语句中的LIMIT关键字,不过相对功能较弱,limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度:

List<Student> civilStudents = students.stream().filter(student -> "xxx".equals(student.getMajor())).limit(2).collect(Collectors.toList());

    sorted:该操作用于对流中元素进行排序,sorted要求待比较的元素必须实现Comparable接口,如果没有实现也不要紧,可以将比较器作为参数传递给sorted(Comparator<? super T> comparator)。

List<Student> sortedCivilStudents = students.stream().filter(student -> "xxxx".equals(student.getMajor())).sorted((s1, s2) -> s1.getAge() - s2.getAge()).limit(2).collect(Collectors.toList());

    skip:skip操作与limit操作相反,如同其字面意思一样,是跳过前n个元素,比如我们希望找出排序在2之后的土木工程专业的学生,那么可以实现为:

List<Student> civilStudents = students.stream().filter(student -> "土木工程".equals(student.getMajor())).skip(2).collect(Collectors.toList());

    通过skip,就会跳过前面两个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。

    映射:在SQL中,借助SELECT关键字后面添加需要的字段名称,可以仅输出我们需要的字段数据,而流式处理的映射操作也是实现这一目的,在java8的流式处理中,主要包含两类映射操作:map和flatMap。

    map:可以在filter筛选的基础之上,通过map将学生实体映射成为学生姓名字符串,具体实现如下:

List<String> names = students.stream().filter(student -> "xxxx".equals(student.getMajor())).map(Student::getName).collect(Collectors.toList());

    mapToDouble:(ToDoubleFunction<? super T> mapper)

    mapToInt:(ToIntFunction<? super T> mapper)

    mapToLong:(ToLongFunction<? super T> mapper)

    这些映射分别返回对应类型的流,直接映射为IntStream,我们可以直接调用提供的sum()方法来达到目的,此外使用这些数值流的好处还在于可以避免jvm装箱操作所带来的性能消耗。

    flatMap:flatMap与map的区别在于 flatMap是将一个流中的每个值都转成一个个流,然后再将这些流扁平化成为一个流 。flatMap将由map映射得到的Stream<String[]>,转换成由各个字符串数组映射成的流Stream<String>,再将这些小的流扁平化成为一个由所有字符串构成的大流Steam<String>,从而能够达到我们的目的。

   与map类似,flatMap也提供了针对特定类型的映射操作:

   flatMapToDouble(Function<? super T,? extends DoubleStream> mapper)

   flatMapToInt(Function<? super T,? extends IntStream> mapper)

   flatMapToLong(Function<? super T,? extends LongStream> mapper)。

    终端操作

    查找

    allMatch:用于检测是否全部都满足指定的参数行为,如果全部满足则返回true。

boolean isAdult = students.stream().allMatch(student -> student.getAge() >= 18);

    anyMatch:则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true。

boolean hasWhu = students.stream().anyMatch(student -> "xxxx".equals(student.getSchool()));

    noneMathch:用于检测是否不存在满足指定行为的元素,如果不存在则返回true。

boolean noneCs = students.stream().noneMatch(student -> "xxxx".equals(student.getMajor()));

    findFirst:用于返回满足条件的第一个元素。findFirst不携带参数,具体的查找条件可以通过filter设置,此外我们可以发现findFirst返回的是一个Optional类型。

Optional<Student> optStu = students.stream().filter(student -> "xxxx".equals(student.getMajor())).findFirst();

    findAny:相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个。实际上对于顺序流式处理而言,findFirst和findAny返回的结果是一样的。当启用并行流式处理的时候,查找第一个元素往往会有很多限制,如果不是特别需求,在并行流式处理中使用findAny的性能要比findFirst好。

Optional<Student> optStu = students.stream().filter(student -> "xxxx".equals(student.getMajor())).findAny();

    归约

    前面是通过collect(Collectors.toList())对数据封装返回,如果目标不是返回一个新的集合,而是希望对经过参数化操作后的集合进行进一步的运算,那么我们可用对集合实施归约操作。java8的流式处理提供了reduce方法来达到这一目的。

    通过mapToInt将Stream<Student>映射成为IntStream,并通过IntStream的sum方法求和,实际上我们通过归约操作,也可以达到这一目的,实现如下:

// 前面例子中的方法 int totalAge = students.stream().filter(student -> "xxxx".equals(student.getMajor())).mapToInt(Student::getAge).sum(); // 归约操作 int totalAge = students.stream().filter(student -> "xxxx".equals(student.getMajor())).map(Student::getAge).reduce(0, (a, b) -> a + b);

    收集:前面利用collect(Collectors.toList())是一个简单的收集操作,是对处理结果的封装,对应的还有toSet、toMap,以满足我们对于结果组织的需求。这些方法均来自于java.util.stream.Collectors,可以称之为收集器。

    归约:收集器也提供了相应的归约操作,但是与reduce在内部实现上是有区别的,收集器更加适用于可变容器上的归约操作,这些收集器广义上均基于Collectors.reducing()实现。

long count = students.stream().collect(Collectors.counting());

    分组:在数据库操作中,我们可以通过GROUP BY关键字对查询到的数据进行分组,java8的流式处理也为我们提供了这样的功能Collectors.groupingBy来操作集合进行分组,groupingBy接收一个分类器Function<? super T, ? extends K> classifier我们可以自定义分类器来实现需要的分类效果:

Map<String, List<Student>> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool));

    上面演示的是一级分组,我们还可以定义多个分类器实现 多级分组,实现如下:

Map<String, Map<String, List<Student>>> groups2 = students.stream().collect( Collectors.groupingBy(Student::getSchool, // 一级分组,按学校 Collectors.groupingBy(Student::getMajor))); // 二级分组,按专业

    实际上在groupingBy的第二个参数不是只能传递groupingBy,还可以传递任意Collector类型,比如我们可以传递一个Collector.counting,用以统计每个组的个数,如果我们不添加第二个参数,则编译器会默认帮我们添加一个Collectors.toList()。:

Map<String, Long> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool, Collectors.counting()));

     分区:分区可以看做是分组的一种特殊情况,在分区中key只有两种情况:true或false,目的是将待分区集合按照条件一分为二,java8的流式处理利用ollectors.partitioningBy()方法实现分区,该方法接收一个谓词分区相对分组的优势在于,我们可以同时得到两类结果,在一些应用场景下可以一步得到我们需要的所有结果,比如将数组分为奇数和偶数。:

并行流式数据处理

    流式处理中的很多都适合采用 分而治之 的思想,从而在处理集合较大时,极大的提高代码的性能,java8的设计者也看到了这一点,所以提供了 并行流式处理。上调用stream()方法来启动流式处理,java8还提供了parallelStream()来启动并行流式处理,parallelStream()本质上基于java7的Fork-Join框架实现,其默认的线程数为宿主机的内核数。启动并行流式处理虽然简单,只需要将stream()替换成parallelStream()即可,但既然是并行,就会涉及到多线程安全问题,所以在启用之前要先确认并行是否值得(并行的效率不一定高于顺序执行),另外就是要保证线程安全。此两项无法保证,那么并行毫无意义,毕竟结果比速度更加重要,以后有时间再来详细分析一下并行流式数据处理的具体实现和最佳实践。

Stream 的特性可以归纳为:

  • 不是数据结构
  • 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
  • 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
  • 所有 Stream 的操作必须以 lambda 表达式为参数
  • 不支持索引访问
  • 你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。不过请参阅下一项。
  • 很容易生成数组或者 List
  • 惰性化
  • 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
  • Intermediate 操作永远是惰性化的。
  • 并行能力
  • 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
  • 可以是无限的
  • 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

猜你喜欢

转载自blog.csdn.net/ZytheMoon/article/details/86544013