Java8:Stream详解

1. Stream概述?

JDK文档:

A sequence of elements supporting sequential and parallel aggregate operations.

中文翻译:

Stream是元素的集合,可以支持顺序和并行的对原Stream进行汇聚的操作;

Stream代表数据流,流中的数据元素的数量可能是有限的,也可能是无限的。

Java为什么要引入Stream?这个问题可以从侧面更好的了解Stream的概念

  • 通过函数式编程的方式可以将将复杂的数据处理过程变得简单明了,那么这个和Stream有什么关系?本质上Streams是Monads。

Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。

  • 提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势

Stream相关概念

  • 一系列元素:Stream对一组有特定类型的元素提供了一个接口。但是Stream并不真正存储元素,元素根据需求被计算出结果。
  • 源:Stream可以处理任何一种数据提供源,比如结合、数组,或者I/O资源。
  • 聚合操作:Stream支持类似SQL一样的操作,常规的操作都是函数式编程语言,比如filter,map,reduce,find,match,sorted,等等。

Stream操作还具备两个基本特性使它与集合操作不同:

  • 管道:许多Stream操作会返回一个stream对象本身。这就允许所有操作可以连接起来形成一个更大的管道。这就就可以进行特定的优化了,比如懒加载和短回路,我们将在下面介绍。
  • 内部迭代:和集合的显式迭代(外部迭代)相比,Stream操作不需要我们手动进行迭代。

总结Stream的特点

  • 不存储数据。流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
  • 函数式编程。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
  • 延迟操作。流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
  • 可以解绑。对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
  • 纯消费。流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,需要重新生成一个新的流。

2. Stream的使用

2.1 流的操作类型

对stream的操作分为为两类,中间操作(intermediate operations)和结束操作(terminal operations)

  • 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。
  • 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。

还有一种操作被称为 short-circuiting。用以指:
对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

2.2 使用Stream的步骤

创建Stream -> 转换Stream每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换) ->对Stream进行聚合(Reduce)操作,获取想要的结果

2.3 Stream创建
  1. 通过Collection的stream()方法或者parallelStream(),比如Arrays.asList(1,2,3).stream()。
  2. 使用流的静态方法,比如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)。
  3. BufferedReader.lines()从文件中获得行的流。
  4. Files类的操作路径的方法,如list、find、walk等。
  5. 随机数流Random.ints()
  6. 通过Arrays.stream(Object[])方法, 比如Arrays.stream(new int[]{1,2,3})。
  7. 其它一些类提供了创建流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()。

追踪到底层其实都是使用StreamSupport类,它提供了将Spliterator转换成流的方法。至于它的内部细节在下片文章介绍。

2.4 中间操作

中间操作会返回一个新的流,但是操作是延迟执行的(lazy),它不会修改原始的数据源,而且是由在终点操作开始的时候才真正开始执行。
这个Scala集合的转换操作不同,Scala集合转换操作会生成一个新的中间集合,显而易见Java的这种设计会减少中间对象的生成。

操作类型 方法
中间操作 concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()

区分中间操作和结束操作最简单的方法,就是看方法的返回值,返回值为stream的大都是中间操作,否则是结束操作。

distinct()

Stream<T> distinct();

对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;

filter()

Stream<T> filter(Predicate<? super T> predicate);

对于Stream中包含的元素使用给定的predicate过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素。

map()

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

对于Stream中包含的元素使用给定的转换函数进行转换操作,对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。

这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;

flatMap()

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

和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;

peek()

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

生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数

skip()

Stream<T> skip(long n);

返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

limit()
对一个Stream进行截断操作,获取其前N个元素。如果原Stream中包含的元素个数小于N,那就获取其所有的元素,这是一个short-circuiting 操作。

下面统一实例代码:

Arrays.asList(1, 1, null, 2, 3, 4, null, 5, 6, 7, 8, 9, 10)
       .stream()
       .distinct()
       .filter(t -> t != null && t > 9)
       .map(t -> t * 10)
       .flatMap(t -> Stream.of(t, t + 1))
       .skip(1)
       .forEach(System.out::println);
2.5 终结操作
操作类型 方法
结束操作 allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

Match

public boolean  allMatch(Predicate<? super T> predicate)
public boolean  anyMatch(Predicate<? super T> predicate)
public boolean  noneMatch(Predicate<? super T> predicate)

这一组方法用来检查流中的元素是否满足断言。

  • allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true
  • anyMatch只有在任意一个元素满足断言时就返回true,否则flase,
  • noneMatch只有在所有的元素都不满足断言时才返回true,否则flase

count

count方法返回流中的元素的数量。它实现为:

forEach/forEachOrdered

forEach遍历流的每一个元素,执行指定的action。和peek方法不同。这个方法不担保按照流的encounter order顺序执行,如果对于有序流按照它的encounter order顺序执行,你可以使用forEachOrdered方法。

max/min

max返回流中的最大值,
min返回流中的最小值。

toArray()

将流中的元素放入到一个数组中。

总结:
终结操作也叫汇聚操作,它接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的汇聚操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。

注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有

汇聚操作可以分为以下两类

  • 可变汇聚:把输入的元素们累积到一个可变的容器中,如collect。

  • 其他汇聚:除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch。

collect和ruduce比较重要,单独一节讲述。

3. reduce

作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于

Integer sum = integers.reduce(0, (a, b) -> a+b); 
//或者
Integer sum = integers.reduce(0, Integer::sum);

下面看它的定义:

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)

举个例子

Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .stream()
        .reduce((a, b) -> a + b)
        .ifPresent(System.out::println);
System.out.println(Stream.of("A", "B", "C", "D")
        .reduce("Str", (a, b) -> a + "-" + b));
Stream.of("I", "love", "you")
        .reduce(0,// 初始值 
                (sum, str) -> sum + str.length(),// 累加操作
                (a, b) -> 0); //并行stream才会用到

4. collect

可变汇聚对应的只有一个方法:collect,正如其名字显示的,它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。看一下它的定义:

 <R> R collect(Supplier<R> supplier,
                 ObjIntConsumer<R> accumulator,
                 BiConsumer<R, R> combiner)

先来看看这三个参数的含义: supplier是一个工厂函数,用来生成一个新的容器, accumulator用来把Stream中的元素添加到结果容器中,BiConsumer<R, R> combiner参数用来把中间状态的多个结果容器合并成为一个(并行的时候会用到)
还有一个重载函数,参数是Collector类型,三个参数太麻烦,收集器Collector就是对这三个参数的简单封装。Collectors工具类可通过静态方法生成各种常用的Collector。

<R, A> R collect(Collector<? super T, A, R> collector);

举个例子:

List list1 = Stream.of(1,2,3,4,5,6,7,8,9,10)
        .collect(() -> new ArrayList<Integer>(),.//生成一个新的ArrayList实例
                //接受两个参数,第一个是前面生成的ArrayList对象,
                //二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。
                //此函数被反复调用直到原stream的元素被消费完毕;
                (list, item) -> list.add(item),
               //接受两个参数,这两个都是ArrayList类型的,函数体就是把第二个ArrayList全部加入到第一个中;
                (lista, listb) -> lista.addAll(listb));
List list2 = Stream.of(1,2,3,4,5,6,7,8,9,10)
        .collect(ArrayList::new,
                ArrayList::add,
                ArrayList::addAll);
List list3 =  Stream.of(1,2,3,4,5,6,7,8,9,10)
        .collect(Collectors.toList());
System.out.println(list3);

5. 总结

Stream的常用API基本介绍完毕,应该有了一个初步的认识。总结一下Stream 的特性:

  • 不是数据结构,它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。所以也不支持索引访问。

  • 所有 Stream 的操作必须以 lambda 表达式为参数

  • 惰性化,很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
    Intermediate 操作永远是惰性化的。

  • 并行能力,当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。

  • 可以是无限的,集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,675评论 18 139
  • 这篇关于java stream的文章写的特别好,转载一下,以备自己查看。转载自Java 8 中的 Streams ...
    小白小白啦阅读 512评论 0 2
  • 1. Stream初体验 我们先来看看Java里面是怎么定义Stream的: A sequence of elem...
    kechao8485阅读 1,238评论 0 9
  • =========================================================...
    高速路边数车车阅读 1,667评论 0 1
  • 冰山古竹字生光, 小鱼问佛意昂扬。 忧罗独怜美人老, 寒舟一杆钓大江。 下面是我手绘…请严肃点
    阎浮小学僧阅读 285评论 15 7