【java8新特性】Stream API详解

欢迎交流java8新特性系列文章:https://www.jianshu.com/nb/27231419 . [1][2][3][4][5]

一、简介

     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方法提供的消费函数在此时执行),这种操作为终端操作。

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


  1. 【java8新特性】lambda表达式与函数式接口详解

  2. 【java8新特性】Stream API详解

  3. 【java8新特性】Optional详解

  4. 【java8新特性】方法引用

  5. 【java8新特性】默认方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容