zoukankan      html  css  js  c++  java
  • 【java8】Stream 特性讲解

    一、简介

         java8新添加了一个特性:流Stream。Stream让开发者能够以一种声明的方式处理数据源(集合、数组等),它专注于对数据源进行各种高效的聚合操作(aggregate operation)和大批量数据操作 (bulk data operation)。

        Stream API将处理的数据源看做一种Stream(流),Stream(流)在Pipeline(管道)中传输和运算,支持的运算包含筛选、排序、聚合等,当到达终点后便得到最终的处理结果。

    几个关键概念

    1. 元素 Stream是一个来自数据源的元素队列,Stream本身并不存储元素。
    2. 数据源(即Stream的来源)包含集合、数组、I/O channel、generator(发生器)等。
    3. 聚合操作 类似SQL中的filter、map、find、match、sorted等操作
    4. 管道运算 Stream在Pipeline中运算后返回Stream对象本身,这样多个操作串联成一个Pipeline,并形成fluent风格的代码。这种方式可以优化操作,如延迟执行(laziness)和短路( short-circuiting)。
    5. 内部迭代 不同于java8以前对集合的遍历方式(外部迭代),Stream API采用访问者模式(Visitor)实现了内部迭代。
    6. 并行运算 Stream API支持串行(stream() )或并行(parallelStream() )的两种操作方式。

    Stream API的特点:

    1. Stream API的使用和同样是java8新特性的 lambda表达式 密不可分,可以大大提高编码效率和代码可读性。
    2. Stream API提供串行和并行两种操作,其中并行操作能发挥多核处理器的优势,使用fork/join的方式进行并行操作以提高运行速度。
    3. Stream API进行并行操作无需编写多线程代码即可写出高效的并发程序,且通常可避免多线程代码出错的问题。

    二、简单示例

        我们来看一个简单的示例,统计整数数组中正数的个数:

    1. 在java8之前:
        public static void main(String[] args)
        {  
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
            
            long count = 0;
            
            for(Integer number: numbers)
            {
                if(number > 0)
                {
                    count++;
                }
            }
            
            System.out.println("Positive count: " + count);
        }
    
    1. 在java8之后:
        public static void main(String[] args)
        {  
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
          
            long count = numbers.parallelStream().filter(i -> i>0).count();
            
            System.out.println("Positive count: " + count);
        }
    

    可以看到,上例中,使用filter()方法对数组进行了过滤,使用count()方法对过滤后的数组进行了大小统计,且使parallelStream()方法为集合创建了并行流,自动采用并行运算提高速度。在更复杂的场景,还可以用forEach()、map()、limit()、sorted()、collect()等方法进行进一步的流运算。

    三、典型接口详解

        本节以典型场景为例,列出Stream API常用接口的用法,并附上相应代码。
        需要说明的是,Stream API中存在很多方法重载,同名方法本文中可能仅列举一个,请读者注意~

    3.1  Stream的生成

        java8 Stream API支持串行或并行的方式,可以简单看下jdk1.8 Collection接口的源码(注释只截取部分):

        /**
         * @return a sequential {@code Stream} over the elements in this collection
         * @since 1.8
         */
        default Stream<E> stream() {
            return StreamSupport.stream(spliterator(), false);
        }
    
        /**
         * @return a possibly parallel {@code Stream} over the elements in this collection
         * @since 1.8
         */
        default Stream<E> parallelStream() {
            return StreamSupport.stream(spliterator(), true);
        }
    

    可以看出,在集合类的接口(Collection)中,分别用两种方式来生成:

            1. 串行流 : stream()
            2. 并行流 : parallelStream()

    应该注意的是,使用parallelStream()生成并行流后,对集合元素的遍历是无序的。

    3.2  forEach()方法

        简单看下forEach()方法的源码(注释只截取部分):

        /**
         * Performs an action for each element of this stream.
         */
        void forEach(Consumer<? super T> action);
    

    forEach()方法的参数为一个Consumer(消费函数,一个函数式接口)对象,forEach()方法用来迭代流中的每一个数据,例如:

        public static void main(String[] args)
        {  
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
            
            numbers.stream().forEach(n ->  System.out.println("List element: " + n));
        }
    

    上例中,对数组的每个元素进行串行遍历,并打印每个元素的值。

    ps:
        集合的顶层接口Iterable中也投forEach方法,可以直接对数组元素进行遍历:

        public static void main(String[] args)
        {  
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
            
            numbers.forEach(n ->  System.out.println("List element: " + n));
        }
    

    当然用Strem API的好处不仅仅是遍历~~~

    3.3  map()方法

        简单看下map()方法的源码(注释只截取部分):

        /**
         * Returns a stream consisting of the results of applying the given function to the elements of this stream.
         * @param <R> The element type of the new stream
         * @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
         *               <a href="package-summary.html#Statelessness">stateless</a>
         *               function to apply to each element
         * @return the new stream
         */
        <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    

    map()方法的参数为Function(函数式接口)对象,map()方法将流中的所有元素用Function对象进行运算,生成新的流对象(流的元素类型可能改变)。举例如下:

        public static void main(String[] args)
        {  
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
            
            numbers.stream().map( n -> Math.abs(n)).forEach(n ->  System.out.println("Element abs: " + n));
        }
    

    上例中,用map()方法计算了所有数组元素的绝对值并生成了一个新的流,然后再用forEach()遍历打印。

    3.4  flatMap()方法

        简单看下flatMap()方法的源码(省略注释):

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

    显然,跟map()方法不同的是,Function函数的返回值类型是Stream<? extends R>类型,而不是R类型,即Function函数返回一个Stream流,这样flatMap()能够将一个二维的集合映射成一个一维的集合,比map()方法拥有更高的映射深度(此处可能有一点绕,可结合例子理解),作个简单示例如下:

    有一个字符串数组:

    List<String> list = Arrays.asList("1 2", "3 4", "5 6");
    

    其有三个元素,每个元素有两个数组并用空格隔开,如果每个元素以空格分割成2个元素,并遍历打印这6个元素,

    用flatMap()方法如下:

    list.stream().flatMap(item -> Arrays.stream(item.split(" "))).forEach(System.out::println);
    

    而用map()方法:

     list.stream().map(item -> Arrays.stream(item.split(" "))).forEach(n ->n.forEach(System.out::println));
    

    可见,用map()方法,返回了一个“流中流”,需要在每个Stream元素遍历时,再加一层forEach进行遍历。

    3.5  filter()方法

        简单看下filter()方法的源码(注释只截取部分):

        /**
         * Returns a stream consisting of the elements of this stream that match the given predicate.
         *
         * <p>This is an <a href="package-summary.html#StreamOps">intermediate operation</a>.
         *
         * @param predicate a <a href="package-summary.html#NonInterference">non-interfering</a>,
         *                  <a href="package-summary.html#Statelessness">stateless</a>
         *                  predicate to apply to each element to determine if it  should be included
         * @return the new stream
         */
        Stream<T> filter(Predicate<? super T> predicate);
    

    filter()方法的参数为Predicate(函数式接口)对象,再lambda表达式的讲解中我们提到过这个接口,一般用它进行过滤。正如第二章中示例:

        public static void main(String[] args)
        {  
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
          
            long count = numbers.parallelStream().filter(i -> i>0).count();
            
            System.out.println("Positive count: " + count);
        }
    

    用filter方法很容易过滤出整数数组中的自然数。

    3.6  reduce()方法

        reduce操作又称为折叠操作,用于将流中的所有值合成一个。reduce()方法的源码(不提供计算初始值的reduce方法)(省略注释):

    Optional<T> reduce(BinaryOperator<T> accumulator);
    

    reduce()方法参数为BinaryOperator类型的累加器(它接受两个类型相同的参数,返回值类型跟参数类型相同),返回一个Optional对象。
     实际上,Stream API中的mapToInt()方法返回的IntStream接口有类似的 average()、count()、sum()等方法就是做reduce操作,类似的还有mapToLong()、mapToDouble() 方法。当然,我们也可以用reduce()方法来自定义reduce操作。例如我们用reduce()方法来进行整数数组求和操作:

        public static void main(String[] args)
        {
            List<Integer> numbers = Arrays.asList(-1, -2, 0, -1, 4, 5, 1);
            
            Integer total = numbers.stream().reduce((t, n) -> t + n).get();
            
            System.out.println("Total: " + total);
        }
    

    上例中利用reduce()方法结合lambda表达式轻易的实现了数组的求和功能。

    3.7  collect()方法

        简单看下collect()方法的源码(注释只截取部分):

        /**
         * @param <R> the type of the result
         * @param <A> the intermediate accumulation type of the {@code Collector}
         * @param collector the {@code Collector} describing the reduction
         * @return the result of the reduction
         */
        <R, A> R collect(Collector<? super T, A, R> collector);
    

    collect()方法的参数为一个java.util.stream.Collector类型对象,可以用java.util.stream.Collectors工具类提供的静态方法来生成,Collectors类实现很多的归约操作,如Collectors.toList()、Collectors.toSet()、Collectors.joining()(joining适用于字符串流)等。看一个简单示例:

        public static void main(String[] args)
        {  
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
            
            List<Integer> abss = numbers.stream().map( n -> Math.abs(n)).collect(Collectors.toList());
            
            System.out.println("Abs list: " + abss);
        }
    

    上例中,用map()方法生成新的流,再用collect()方法返回原数组的绝对值数组。

    3.8  summaryStatistics()方法进行数值统计

        其实summaryStatistics()方法并不是Stream接口的方法,而是Stream API采用mapToInt()、mapToLong()、mapToDouble()三个方法分别生成IntStream 、LongStream 、DoubleStream 三个接口类型的对象,这个方法的参数分别为3个函数式接口ToIntFunction、ToLongFunction、ToDoubleFunction,使用时可以用lambda表达式计算返回对应的int、long、double类型即可,简单看下这三个方法的源码(省略注释):

        IntStream mapToInt(ToIntFunction<? super T> mapper);
    
        LongStream mapToLong(ToLongFunction<? super T> mapper);
    
        DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
    

    IntStream 、LongStream 、DoubleStream 三个接口类型都有一个summaryStatistics()方法,其中,

    1. IntStream 的方法是:
     IntSummaryStatistics summaryStatistics();
    
    1. LongStream 的方法是:
     LongSummaryStatistics summaryStatistics();
    
    1. DoubleStream 的方法是:
     DoubleSummaryStatistics summaryStatistics();
    

    在IntSummaryStatistics、LongSummaryStatistics 、DoubleSummaryStatistics 三个接口类型(位于java.util包下)中,都有诸如统计数量、最大值、最小值、求和、平均值等方法(方法名和返回类型可能不同),利用这些方法我们可以方便的进行数值统计。以IntSummaryStatistics工具包 为例:

        public static void main(String[] args)
        {
            List<Integer> numbers = Arrays.asList(-1, -2, 0, 4, 5);
            
            IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
            
            System.out.println("Max : " + stats.getMax());
            System.out.println("Min : " + stats.getMin());
            System.out.println("Sum : " + stats.getSum());
            System.out.println("Average : " + stats.getAverage());
            System.out.println("Count : " + stats.getCount());
        }
    

    3.9  其它方法

        Stream API还有一些其它的方法,比如:
        limit()    获取指定数量的流
        sorted()   对流进行排序
        distinct()  去重
        skip()    跳过指定数量的元素
        peek()   生成一个包含原Stream的所有元素的新Stream,并指定消费函数
        count()   计算元素数量
        ......

    四、注意事项

    Stream中的操作从概念上讲分为中间操作和终端操作

    • 中间操作:例如peek()方法提供Consumer(消费)函数,但执行peek()方法时不会执行Consumer函数,而是等到流真正被消费时(终端操作时才进行消费)才会执行,这种操作为中间操作;
    • 终端操作:例如forEach()、collect()、count()等方法会对流中的元素进行消费,并执行指定的消费函数(peek方法提供的消费函数在此时执行),这种操作为终端操作。

    要理解中间操作和终端操作的概念,防止埋坑~

    参考:

    廖雪峰stream讲解

     
  • 相关阅读:
    1、编写一个简单的C++程序
    96. Unique Binary Search Trees
    python 操作redis
    json.loads的一个很有意思的现象
    No changes detected
    leetcode 127 wordladder
    django uwsgi websocket踩坑
    you need to build uWSGI with SSL support to use the websocket handshake api function !!!
    pyinstaller 出现str error
    数据库的读现象
  • 原文地址:https://www.cnblogs.com/sowhat1412/p/12734101.html
Copyright © 2011-2022 走看看