美文网首页
Java8 Stream 入门看这篇就够了

Java8 Stream 入门看这篇就够了

作者: 虐心笔记 | 来源:发表于2022-01-27 15:54 被阅读0次

    初识 Stream

    Java 8 API添加了一个新的抽象称为流 Stream,可以让你以一种声明的方式处理数据。使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

    Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

    图上的流程转换为 Java 代码为:

    List<Integer> transactionsIds = 
    widgets.stream()  // Source
                 .filter(b -> b.getColor() == RED)  // Operations-filter
                 .sorted((x,y) -> x.getWeight() - y.getWeight())   // Operations-sorted
                 .mapToInt(Widget::getWeight)   // Operations-map
                 .sum();  // Results-sum
    

    什么是 Stream?

    Stream(流)是一个来自数据源的元素队列并支持聚合操作

    • 无存储:不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
    • 为函数式编程而生:对 stream 的修改都不会修改其数据源,比如对 stream 执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新 stream。 (peek 对可变对象可以修改)
    • 惰式执行:stream 上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
    • 可消费性:stream 只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

    Stream操作还有两个基础的特征:

    • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
    • 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。

    关于 Stream 的相关API实现原理, 感兴趣的可以去看下: 浅析 Java8 Stream 原理

    创建流

    stream 常见的创建的方式主要有Arrays、Collection、Stream 静态方法等,这里代码列举其中最常见的几种:

    package com.company.designModel;
    
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.stream.Stream;
    
    public class Java8StreamTest {
    
        public static void main(String[] args) {
            // 1.数组创建流
            Integer[] arrays = {9, 5, 2, 7};
            Stream<Integer> arraysStream = Arrays.stream(arrays);
    
            // 2.Collection 创建流
            ArrayList<Object> list = new ArrayList<>();
            list.add("Amy");
            list.add("Angst");
            Stream<Object> stream = list.stream();
            Stream<Object> parallelStream = list.parallelStream();  // 这里 parallelStream 创建的是一个并行流
    
            // 3. Stream.of
            Stream.of(1, 24, 8, 6, 10, 4, 8, 3, 2, 8, 6).skip(2).limit(5).forEach(System.out::println);
            // empty() 可以创建一个空的流
            Stream<Object> emptyStream = Stream.empty();
    
            // 4.Stream.generate()
            Stream.generate(Math::random).limit(10).skip(1).forEach(System.out::print);
    
            // 5.Stream.iterate()
            Stream.iterate(0, item -> ++ item).limit(10).forEach(System.out::print);
    
            // 6.Stream.builder()
            Stream<Object> buildStream = Stream.builder().add("Amy").add("Angst").build();
    
            // 7. Stream.concat() 合并创建一个新的流
            Stream<Object> concatStream = Stream.concat(stream, parallelStream);
        }
    }
    
    

    这里有几个需要关注的点:

    • 查看 Stream 源码的话,你会发现 of() 方法内部其实调用了 Arrays.stream() 方法。源码注释有提到 “从数组创建的流是安全的”
        /**
         * Returns a sequential ordered stream whose elements are the specified values.
         *
         * @param <T> the type of stream elements
         * @param values the elements of the new stream
         * @return the new stream
         */
        @SafeVarargs
        @SuppressWarnings("varargs")   // Creating a stream from an array is safe
        public static<T> Stream<T> of(T... values) {
            return Arrays.stream(values);
        }
    
    • Stream.generate(): 生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象), 其中值是随机的。这个无限长度 Stream 是懒加载,一般都会配合Stream的limit()方法来做分页使用。
    • Stream.iterate(): 也是生成无限长度的Stream,和 generator 不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的. 其中包含的元素可以认为是:seed,f(seed), f(f(seed))无限循环。

    操作流

    流的操作类型分为两种:

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

      • 有状态(Stateless):指该操作只有拿到所有元素之后才能继续下去
      • 无状态(Stateful):指元素的处理不受之前元素的影响
    • 终止操作(Terminal):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以,这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

      • 非短路操作: 指必须处理所有元素才能得到最终结果
      • 短路操作: 指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果

    代码演示

    Intermediate方法
    • filter(): 用于筛选数据,返回由匹配的元素组成的流。
    • limit(): 切片限流可以当做分页来使用,返回被截断的流。
    • skip():要跳过前面元素n的数量,返回由该流的剩余元素组成的流。
    • distinct():去重操作,返回由不同元素组成的流。
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Vivian");
        list.add("Amy");
        list.add("Amy");
        list.add("Angst");
        list.add("Zx");
    
        list.stream().  // 创建一个list流
                distinct().  // 去重操作
                skip(1).  // 跳过第一个元素
                filter(o -> o.startsWith("A")).  // 过滤以A开头的元素
                limit(2).  // 切片选择保留几个
                forEach(System.out::println);  // 输出结果
    
    }
    
    结果输出:
    
    Amy
    Angst
    
    Process finished with exit code 0
    
    • map():接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
    • flatMap():接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Vivian");
        list.add("Amy");
        list.add("Angst");
    
        // 1.map
        list.stream().map(String::toLowerCase).forEach(System.out::println);
        list.stream().mapToInt(String::length).forEach(System.out::println);
        list.stream().mapToDouble(String::length).forEach(System.out::println);
        list.stream().mapToLong(String::length).forEach(System.out::println);
    
        // 2. flatMap 同 map 作用类似,区别就是将每个元素重新组成Stream,并将这些Stream 串行合并成一条Stream.
        list.stream().flatMap(o-> Stream.of(o.toLowerCase())).forEach(System.out::println);
        list.stream().flatMapToInt(o-> IntStream.of(o.length())).forEach(System.out::println);
        list.stream().flatMapToDouble(o-> DoubleStream.of(o.length())).forEach(System.out::println);
        list.stream().flatMapToLong(o-> LongStream.of(o.length())).forEach(System.out::println);
    
    }
    
    结果输出:
    
    vivian
    amy
    angst
    6
    3
    5
    6.0
    3.0
    5.0
    6
    3
    5
    
    vivian
    amy
    angst
    6
    3
    5
    6.0
    3.0
    5.0
    6
    3
    5
    
    Process finished with exit code 0
    
    • sorted():自然排序,流中元素需实现Comparable接口, 也可以自定义 Comparator
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Vivian");
        list.add("Amy");
        list.add("Angst");
    
        list.stream().sorted().forEach(System.out::println);  // 默认排序
        list.stream().sorted((x, y) -> y.length() - x.length()).forEach(System.out::println);  // 自定义排序
        
    }
    
    结果输出:
    Amy
    Angst
    Vivian
    
    Vivian
    Angst
    Amy
    
    Process finished with exit code 0
    
    • peek():如同于map(),能得到流中的每一个元素。但map接收的是一个 Function 表达式,有返回值。而peek接收的是Consumer,没有返回值。
    public class Java8StreamTestDemo {
    
        public static void main(String[] args) {
            // 1.不可变对象
            ArrayList<String> arrayList = new ArrayList<>();
            arrayList.add("Vivian");
            arrayList.add("Amy");
            arrayList.add("Angst");
    
            // 修改元素首字母小写,实际结果并没有修改成功
            arrayList.stream().peek(String::toLowerCase).forEach(o -> System.out.println("toLowerCase:" + o));  
    
    
            // 2.可变对象
            ArrayList<Food> list = new ArrayList<>();
            list.add(new Food("果汁"));
            list.add(new Food("奶茶"));
            list.add(new Food("牛奶"));
    
            // peek()方法存在的主要目的是用调试,通过 peek() 方法可以看到流中的数据经过每个处理点时的状态。
            list.stream().peek(o -> System.out.println("debugger: "+ o.getCoca())).count();
    
            // 除去用于调试,peek()在需要修改元素内部状态的场景也非常有用,比如修改 Coca 的值 ,当然也可以使用map()和flatMap实现.
            list.stream().peek(o->o.setCoca("饭前:"+ o.getCoca())).forEach(System.out::println);
    
    
        }
        
        // 定义一个可变对象
        static class Food {
            private String coca="饮料";
            public Food(String coca) { this.coca = coca; }
            public String getCoca() { return coca; }
            public void setCoca(String coca) { this.coca = coca; }
    
            @Override
            public String toString() {
                return "Food{" +
                        "coca='" + coca + '\'' +
                        '}';
            }
        }
    
    }
    
    结果输出:
    
    toLowerCase:Vivian
    toLowerCase:Amy
    toLowerCase:Angst
    debugger: 果汁
    debugger: 奶茶
    debugger: 牛奶
    Food{coca='饭前:果汁'}
    Food{coca='饭前:奶茶'}
    Food{coca='饭前:牛奶'}
    
    Process finished with exit code 0
    

    注意:map函数对Stream中元素执行的是映射操作,会以新的元素(map的结果)填充新的Stream。严格的讲map不是修改原来的元素。peek只能消费Stream中的元素,是否可以更该Stream中的元素,取决于Stream中的元素是否是不可变对象。如果是不可变对象,则不可修改Stream中的元素;如果是可变对象,则可以修改对象的值,但是无法修改对象的引用。


    Terminal方法
    • max(): 返回此流的最大元素
    • min(): 返回此流的最小元素
    • count(): 返回此流中的元素计数
    • findFirst(): 返回此流中第一个元素
    • findAny(): 取任意一个元素,正常情况下一般会取第一个元素,在并行流的情况下会随机取一个元素
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("Vivian");
        arrayList.add("Amy");
        arrayList.add("Angst");
    
        System.out.println(arrayList.stream().map(String::length).max(Integer::compareTo));
        System.out.println(arrayList.stream().map(String::length).min(Integer::compareTo));
        System.out.println(arrayList.stream().map(String::length).count());
    
        System.out.println(arrayList.stream().map(String::length).findFirst());
        // 在 parallel 流中存在每次输出的结果不一致
        System.out.println(arrayList.stream().parallel().map(String::length).findAny());
    }
    
    结果输出:
    
    Optional[6]
    Optional[3]
    3
    
    Optional[6]
    Optional[3]
    
    Process finished with exit code 0
    
    • andMatch(): 返回流中的元素是否与所提供的匹配
    • allMatch(): 返回流的所有元素是否与指定的条件匹配。
    • noneMatch(): 返回流中是否没有匹配谓词的元素
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("Vivian");
        arrayList.add("Amy");
        arrayList.add("Angst");
    
        // 匹配成功返回 true, 否则 false
        System.out.println(arrayList.stream().noneMatch(o->o.equals("amy")));  // 空匹配
        System.out.println(arrayList.stream().anyMatch(o->o.equals("Amy")));  // 任意匹配
        System.out.println(arrayList.stream().allMatch(o->o.equals("Amy")));  // 全匹配
    }
    
    结果输出:
    
    true
    true
    false
    
    Process finished with exit code 0
    
    • reduce(): 用于对 stream 中元素进行聚合求值
    // 规约操作
    System.out.println(Stream.of(1, 9, 5, 2, 7, -1).reduce(0, Integer::sum));
    
    
    23
    
    Process finished with exit code 0
    
    • forEach(): 遍历元素
    • iterator(): 返回一个迭代器
    public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<>();
        arrayList.add("Vivian");
        arrayList.add("Amy");
        arrayList.add("Angst");
    
        // 遍历元素
        arrayList.stream().forEach(System.out::println);
        
        // 返回的是一个可迭代对象
        System.out.println(arrayList.stream().iterator());
    }
    
    结果输出:
    
    
    Vivian
    Amy
    Angst
    
    java.util.Spliterators$1Adapter@7ba4f24f
    
    Process finished with exit code 0
    
    • toArray(): 返回一个数组
    • collect(): 收集操作,将一个对象的集合转化成另一个对象的集合
    // 把流转换成数组
    Object[] toArray = Stream.of(1, 9, 5, 2, 7, -1).toArray();
    
    // 使用系统提供的收集器可以将最终的数据流收集到 List Set Map等容器中
    List<Integer> list = Stream.of(1, 9, 5, 2, 7, -1).collect(Collectors.toList());
    

    最后

    以上就是本次的学习分享,Stream 的熟练使用可以让你的代码更加简洁易于维护,以上案例中可能还涉及其他的知识点,比如函数式接口、lambda等,感兴趣的可以看下笔者其他的学习笔记。欢迎留言讨论

    相关文章

      网友评论

          本文标题:Java8 Stream 入门看这篇就够了

          本文链接:https://www.haomeiwen.com/subject/oyinhrtx.html