当前位置 博文首页 > 说故事的五公子:此流非彼流——Stream详解

    说故事的五公子:此流非彼流——Stream详解

    作者:说故事的五公子 时间:2021-01-30 18:16

    Stream是什么?

    Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。

    Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

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

    Stream和IO包下的InputStream和OutputStream一样吗?

    划重点:这个Stream不同于java.ioInputStreamOutputStream,它代表的是任意Java对象的序列。两者对比如下:

    java.io java.util.stream
    存储 顺序读写的bytechar 顺序输出的任意Java对象实例
    用途 序列化至文件或网络 内存计算/业务逻辑

    这时候大家可能又有疑问了,那么既然是顺序输出的任意Java对象实例,那么和List集合不就相同了吗?

    再次划重点:这个StreamList也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。

    换句话说,List的用途是操作一组已存在的Java对象,而Stream实现的是惰性计算,两者对比如下:

    java.util.List java.util.stream
    元素 已分配并存储在内存 可能未分配,实时计算
    用途 操作一组已存在的Java对象 惰性计算

    关于惰性计算在下面的章节中可以看到。

    Stream特点

    Stream接口还包含几个基本类型的子接口如IntStream, LongStream 和 DoubleStream。

    特点:

    • 不存储数据:流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
    • 函数式编程:流的操作不会修改数据源,例如filter不会将数据源中的数据删除。
    • 延迟操作:流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
    • 纯消费:流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。

    Stream的创建

    Stream的创建有多种方式,下面给大家一一列举出来

    1、Stream.of()

    这种方式一般不常用的,但是测试的时候比较方便

    import java.util.stream.Stream;
    
    public class StreamTest {
        public static void main(String[] args) {
            Stream<String> stream = Stream.of("1", "2", "3", "4");
            //forEach()方法相当于内部循环调用
            //参数的写法是Lambda表达式
            stream.forEach(s -> System.out.println(s));
        }
    }
    

    关于Lambda表达式,在我的这篇博客中有详细介绍,感兴趣的朋友可以去看一下

    2、基于数组或者Collection

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    public class StreamTest {
        public static void main(String[] args) {
            Stream<String> stream1 = Arrays.stream(new String[] { "1", "2", "3" });
            Stream<String> stream2 = List.of("X", "Y", "Z").stream();
            stream1.forEach(System.out::println);
            stream2.forEach(System.out::println);
        }
    }
    

    这两种创建Stream的方式是我们工作中经常会用到的方式,借助Stream(转化、聚合等方法)可以帮助我们更方便的去输出我们想要的结果

    3、其他方式

    • 使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator),如Stream.iterate(0, n -> n * 2),或者generate(Supplier<T> s)Stream.generate(Math::random)

    • BufferedReader.lines()从文件中获得行的流。

    • Files类的操作路径的方法,如listfindwalk等。

    • 随机数流Random.ints()

    • 其它一些类提供了创建流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()

    • 更底层的使用StreamSupport,它提供了将Spliterator转换成流的方法。

    Stream常用API(中间操作)

    还记得我们在前面介绍Stream的时候提到了一个惰性计算。惰性计算的特点是:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生。中间操作会返回一个新的流,它不会修改原始的数据源,而且是由在终点操作开始的时候才真正开始执行。

    1、distinct

    distinct保证输出的流中包含唯一的元素,它是通过Object.equals(Object)来检查是否包含相同的元素。

    import java.util.stream.Stream;
    
    public class StreamTest {
        public static void main(String[] args) {
            Stream<String> stream = Stream.of("a", "b", "c", "b","c","d").distinct();
            stream.forEach(System.out::println);
        }
    }
    
    //输出结果
    a
    b
    c
    d
    

    2、filter

    从字面看是过滤的意思,过滤掉不满足条件的数据

    import java.util.stream.IntStream;
    
    public class StreamTest {
        public static void main(String[] args) {
            IntStream stream = IntStream.range(1, 10).filter(i -> i % 2 == 0); //filter中的参数是过滤条件 
            stream.forEach(System.out::println);
        }
    }
    
    //输出结果
    2
    4
    6
    8
    

    3、map

    map方法可以将流中的值映射成另外的值,比如将字符串全部转化成小写

    import java.util.stream.Stream;
    
    public class StreamTest {
        public static void main(String[] args) {
            Stream<String> stream = Stream.of("Hello WORLD HELLO Life").map(s -> s.toLowerCase()); 
            stream.forEach(System.out::println);
        }
    }
    
    //输出结果
    hello world hello life
    

    从输出结果我们可以看到,字符串全部转化成小写字符了

    4、limit

    limit方法指定流的元素数列,类似于Mysql中的limit方法

    import java.util.stream.Stream;
    
    public class StreamTest {
        public static void main(String[] args) {
            Stream<String> stream = Stream.of("1", "2", "3", "4", "5", "6").limit(3); //取三条
            stream.forEach(System.out::println);
        }
    }
    
    // 输出结果
    1
    2
    3
    

    5、peek

    import java.util.stream.Stream;
    
    public class StreamTest {
        public static void main(String[] args) {
            Stream<String> stream = Stream.of("Hello WORLD HELLO Life").peek(s -> {
                String peek = s.toLowerCase();
                System.out.println(peek);
            });
            stream.forEach(System.out::println);
        }
    }
    
    //输出结果
    hello world hello life
    Hello WORLD HELLO Life
    

    有没有发现出一些东西?

    我们将这段代码用上面的map方法实现一下

    import java.util.stream.Stream;
    
    public class StreamTest {
        public static void main(String[] args) {
            Stream<String> stream = Stream.of("Hello WORLD HELLO Life").map(s -> {
                String peek = s.toLowerCase();
                System.out.println(peek);
                return peek;
            });
            stream.forEach(System.out::println);
        }
    }
    
    // 输出结果
    hello world hello life
    hello world hello life
    

    peek方法的定义如下:

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

    peek方法接收一个Consumer的入参。了解λ表达式的应该明白 Consumer的实现类 应该只有一个方法,该方法返回类型为void。

    而map方法的入参为 Function。

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

    我们发现Function 比 Consumer 多了一个 return。这也就是peek 与 map的区别了。

    6、skip

    skip返回丢弃了前n个元素的流,如果流中的元素小于或者等于n,则返回空的流。

    7、sorted

    sorted()将流中的元素按照自然排序方式进行排序

    import java.util.stream.Stream;
    
    public class QueryTest {
    
        public static void main(String[] args) {
    
            //自定义排序
            customSort();
            //自然排序
            naturalSort();
    
        }
    
        public static void customSort() {
            Stream stream = Stream.of("hello", "I", "love", "you").sorted((str1, str2) -> {
                // 自定义排序规则
                if (str1 == null) {
                    return -1;
                }
                if (str2 == null) {
                    return 1;
                }
                return str1.length() - str2.length();
            });
            System.out.println("-----------自定义排序-----------");
            stream.forEach(System.out::println);
        }
    
        public static void naturalSort() {
            Stream<String> stream = Stream.of("hello", "I", "love", "you").sorted();
            System.out.println("-----------自然排序------------");
            stream.forEach(System.out::println);
        }
    
    }
    
    
    // 输出结果
    -----------自定义排序-----------
    I
    you
    love
    hello
    -----------自然排序------------
    I
    hello
    love
    you
    

    如果我们直接调用sorted()方法,那么将按照自然排序,如果我们希望元素按照我们想要的结果来排序,需要自定义排序方法,sorted(Comparator<? super T> comparator)可以指定排序的方式。如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。

    Stream常用API(终点操作)

    1、max、min、count

    max:获取最大值

    min:获取最小值

    count:返回流的数量

    2、reduce

    reduce操作可以实现从一组元素中生成一个值,max()min()count()等都是reduce操作,将他们单独设为函数只是因为常用。reduce()的方法定义有三种重写形式:

    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)
    

    3、count

    获取Stream数量

    package com.mybatisplus;
    
    import java.util.stream.Stream;
    
    public class QueryTest {
    
        public static void main(String[] args) {
            long count = Stream.of("a", "b", "A", "a", "c", "a").count();
            System.out.println(count);
        }
    
    }
    
    //输出结果  6
    

    4、Match

    anyMatch表示,判断的条件里,任意一个元素成功,返回true

    allMatch表示,判断条件里的元素,所有的都是,返回true

    noneMatch跟allMatch相反,判断条件里的元素,所有的都不是,返回true

    package com.mybatisplus;
    
    import java.util.stream.Stream;
    
    public class QueryTest {
    
        public static void main(String[] args) {
            boolean b1 = Stream.of("a", "b", "A", "a", "c", "a").anyMatch(str -> str.equals("a"));
            boolean b2 = Stream.of("a", "b", "A", "a", "c", "a").allMatch(str -> str.equals("a"));
            boolean b3 = Stream.of("a", "b", "A", "a", "c", "a").noneMatch(str -> str.equals("a"));
    
            System.out.println("b1 = " + b1);
            System.out.println("b2 = " + b2);
            System.out.println("b3 = " + b3);
        }
    
    }
    
    // 输出结果
    b1 = true
    b2 = false
    b3 = false
    
    bk