【小家java】java8新特性之---Stream API 详解 (Map-reduce、Collectors收集器、并行流、groupby多字段分组)(上)

简介: 【小家java】java8新特性之---Stream API 详解 (Map-reduce、Collectors收集器、并行流、groupby多字段分组)(上)

我们为什么需要 Stream API


Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。


集合讲的是数据,流讲的是计算


image.png


Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。


同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。


浅谈聚合操作(Stream API能协助解决)


在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合操作来完成诸如:


   客户每月平均消费金额


   最昂贵的在售商品


   取十个数据样本作为首页推荐


但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。


这个时候,如果没有Java8提供的Stream API,那简直就是噩梦。在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快。


对Stream进一步理解


简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。


Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。


对于 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。


Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。


Java 的并行 API 演变历程基本如下:


1.1.0-1.4 中的 java.lang.Thread


2.5.0 中的 java.util.concurrent


3.6.0 中的 Phasers 等


4.7.0 中的 Fork/Join 框架


5.8.0 中的 Stream

Stream 的另外一大特点是,数据源本身可以是无限的(即无限流)。


对流的操作概述


流的操作类型分为两种:


Intermediate(中间操作):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。


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


还有一种操作被称为 short-circuiting(短路操作)。用以指:


    对于一个 intermediate 操作,如果它接受的是一个无限流,它可以返回一个有限的新 Stream。


   对于一个 terminal 操作,如果它接受的是一个无限流,但能在有限的时间计算出结果。


anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit


下面汇总了Stream所有的操作:


image.png


IntStream、LongStream、DoubleStream


IntStream、LongStream、DoubleStream。当然我们也可以用 Stream、Stream >、Stream,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。


Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。


数值流的构造:

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);


range,需要传入开始节点和结束节点两个参数,返回的是一个有序的LongStream。包含开始节点和结束节点两个参数之间所有的参数,间隔为1.

rangeClosed的功能和range类似。差别就是rangeClosed包含最后的结束节点,range不包含。


进阶:自己生成流(无限流)


  public static<T> Stream<T> generate(Supplier<T> s) {
        Objects.requireNonNull(s);
        return StreamSupport.stream(
                new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
    }


可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。


生成 10 个随机整数:


/////

public static void main(String[] args) {
        Stream.generate(new Random()::nextInt).limit(10).forEach(System.out::println);
        //采用IntStream流的方式(推荐使用 逼格很高)
        IntStream.generate(() -> (int) (System.nanoTime() % 100)).
                limit(10).forEach(System.out::println);
    }

另外一种方式自己生成流(这个非常好用):


public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {}


iterate 跟 reduce 操作很像,接受一个种子值,和一个 UnaryOperator(例如 f)。然后种子值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。


如生成一个等差数列:

Stream.iterate(0, n -> n + 3).limit(10).forEach(x -> System.out.print(x + " ")); //0 3 6 9 12 15 18 21 24 27 

与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。


Stream实操案例


创建流Stream


Java8 中的 Collection 接口被扩展,提供两个获取流的方法 :


   default Stream stream() : 返回一个顺序流


   default Stream parallelStream() : 返回一个并行流


由数组创建流

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流 :static Stream stream(T[] array) : 返回一个流

重载形式,能够处理对应基本类型的数组IntStream/LongStream/DoubleStream :


由值创建流

可以使用静态方法 Stream.of(), 通过显示值创建一个流,它可以接收任意数量的参数:public static Stream of(T… values) : 返回一个流


由方法创建流 : 创建无限流

可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流

迭代

public static Stream iterate(final T seed, final UnaryOperator f)

生成

public static Stream generate(Supplier s)


Stream的中间操作实操


多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。而在终止操作时一次性全部处理,称为**“惰性求值”**


   筛选与切片系列


image.png

emps.parallelStream().filter((e) -> e.getSalary() >= 5000).skip(2).forEach(System.out::println);


这块相对比较简单,就一笔带过了。


peek方法用得比较少,这里特殊介绍一下:


Stream.of("one", "two", "three", "four").peek(e -> System.out.println(e));
输出:这样不会有任何的输出;
Stream.of("one", "two", "three", "four").peek(e -> System.out.println(e)).collect(Collectors.toList());
输出:
one
two
three
four
Stream.of("one", "two", "three", "four")
    .peek(e -> System.out.println("Peeked value: " + e))
    .map(String::toUpperCase)
    .peek(e -> System.out.println("Mapped value: " + e))
    .collect(Collectors.toList());
输出:
Peeked value: one
Mapped value: ONE
Peeked value: two
Mapped value: TWO
Peeked value: three
Mapped value: THREE
Peeked value: four
Mapped value: FOUR


这个说白了。当元素被消费的时候,就会触发peek。有多少个就触发多少次。


unordered的使用案例:

public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
        list.stream().forEach(System.out::print); //123456789
        System.out.println();
        //使用unordered之后输出
        list.stream().unordered().forEach(System.out::print); //123456789
    }


我们会发现,输出的顺序没有改变。所以它并不是来打乱这个顺序的。所以大家使用的时候不要误解了。正确的使用 姿势:

//使stream无序:对于 distinct() 和 limit() 等方法,如果不关心顺序,则可以使用并行:
LongStream.rangeClosed(5, 10).unordered().parallel().limit(3);
IntStream.of(14, 15, 15, 14, 12, 81).unordered().parallel().distinct();
这样使用,能提高CPU的利用率,进而提高处理的效率




相关文章
|
13天前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
36 2
|
15天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
33 3
|
15天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
26 2
|
16天前
|
存储 算法 Java
Java Set因其“无重复”特性在集合框架中独树一帜
【10月更文挑战第14天】Java Set因其“无重复”特性在集合框架中独树一帜。本文深入解析Set接口及其主要实现类(如HashSet、TreeSet)如何通过特定的数据结构(哈希表、红黑树)确保元素唯一性,并提供最佳实践建议,包括选择合适的Set实现类和正确实现自定义对象的`hashCode()`与`equals()`方法。
25 3
|
20天前
|
安全 Java API
Java 17新特性让你的代码起飞!
【10月更文挑战第4天】自Java 8发布以来,Java语言经历了多次重大更新,每一次都引入了令人兴奋的新特性,极大地提升了开发效率和代码质量。本文将带你从Java 8一路走到Java 17,探索那些能让你的代码起飞的关键特性。
67 1
|
7天前
|
Java API 数据处理
探索Java中的Lambda表达式与Stream API
【10月更文挑战第22天】 在Java编程中,Lambda表达式和Stream API是两个强大的功能,它们极大地简化了代码的编写和提高了开发效率。本文将深入探讨这两个概念的基本用法、优势以及在实际项目中的应用案例,帮助读者更好地理解和运用这些现代Java特性。
|
11天前
|
存储 Java API
优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。
【10月更文挑战第19天】本文介绍了如何优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。内容包括Map的初始化、使用Stream API处理Map、利用merge方法、使用ComputeIfAbsent和ComputeIfPresent,以及Map的默认方法。这些技巧不仅提高了代码的可读性和维护性,还提升了开发效率。
32 3
|
11天前
|
存储 安全 Java
Java Map新玩法:深入探讨HashMap和TreeMap的高级特性
【10月更文挑战第19天】Java Map新玩法:深入探讨HashMap和TreeMap的高级特性,包括初始容量与加载因子的优化、高效的遍历方法、线程安全性处理以及TreeMap的自然排序、自定义排序、范围查询等功能,助你提升代码性能与灵活性。
20 2
|
17天前
|
Java 开发者
在Java的集合世界里,Set以其独特的特性脱颖而出,它通过“哈希魔法”和“红黑树防御”两大绝技
【10月更文挑战第13天】在Java的集合世界里,Set以其独特的特性脱颖而出。它通过“哈希魔法”和“红黑树防御”两大绝技,有效抵御重复元素的侵扰,确保集合的纯洁性和有序性。无论是“人海战术”还是“偷梁换柱”,Set都能从容应对,成为开发者手中不可或缺的利器。
30 6
|
15天前
|
Java 开发者
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素
在Java集合世界中,Set以其独特的特性脱颖而出,专门应对重复元素。通过哈希表和红黑树两种模式,Set能够高效地识别并拒绝重复元素的入侵,确保集合的纯净。无论是HashSet还是TreeSet,都能在不同的场景下发挥出色的表现,成为开发者手中的利器。
24 2