Java Stream简介和使用参考

1. 什么是Java Stream

Java Stream是Java 8引入的一个新的集合操作功能类,提供数据的流式处理,能够对Java集合数据实现类似大数据领域中map-reduce的数据处理方式,为集合对象进行各种高效的聚合和批量处理操作提供便利。Java Stream是Java语言在集合类Array/Collection/Map之后又一项集合功能扩展。

Java Stream提供串行和并行的数据处理模式,会在流式操作前后步骤中进行优化处理,可以在并发模式中充分利用多核处理器,实现高效的数据处理过程。

Java Stream不同于其它Java集合类(例如Array/Collection/Map等等),有其独自的特点,

  • 没有数据存储,Java Stream关注的是算法和计算,提供的是数据处理管道(pipeline)。
  • 数据流,数据源转换成Stream数据对象之后,Stream数据处理产生新的Stream数据对象,如此循环直到产生数据结果,整个处理的过程中不改变数据源。
  • 延后处理(Laziness-seeking),很多Stream操作,比如filter/map/duplicate removal,能够将多次操作优化合并后执行,延后处理提供了这种优化执行的机会,详情见下文。
  • 数据源可以是无限的,只要处理过程存在终止条件(例如limit/findFirst)。
  • 一次性消费,在一个Stream操作流水线中,数据源中每一个数据元素只被消费处理一次。如果想再消费,需重新构建一个新的Stream对象。

2. 简单的使用

下面是一个使用Java Stream的样例,使用了Stream的过滤-映射-聚合(filter-map-reduce)操作,

Integer sum = IntStream.range(0,10)
                       .filter(n -> n%2 == 0)
                       .map(n -> n*2)
                       .reduce(0, (x, y) -> x+y);

在这个样例中,首先拿到 [0,9] 十个数字的整形数据集合,然后过滤出偶数,然后对每个偶数乘2,最后算出总和为40。

3. Java Steam的使用演示

Java Stream各种操作方法演示代码请见这里,供使用时参考。请先通过如下git clone命令下载代码仓库,

git clone git@gitee.com:pphh/blog.git

演示代码放在文件夹171124_java_stream中,请打开演示代码项目,运行主程序。

里面包括如下几个部分的演示,

  1. 演示简单的stream集合处理(SimpleStream)
    • 在SimpleStream的演示方法中,通过filter-map-reduce操作数组,获取偶数数列,相加出总和。
  2. 演示Stream的构建操作(StreamBuilder.demo)
    • 通过Java数组生成Stream对象
    • 通过Java Collection集合类生成Stream对象
    • 通过Stream静态工厂类生成Stream对象
  3. 演示Stream的各种中间操作方法:StreamOpertions
    • Stream.filter:在数列中找到偶数,挑出字符串长度大于5的用户名
    • Stream.map:对用户名进行字符串大写处理,对多个集合合并处理
    • Stream.forEach:对集合元素一一执行指定动作,forEach是一个结束操作,只能在最后执行,并只被执行一次。
    • Stream.peek:对集合元素一一执行指定动作,和forEach不一样之处,peek是一个中间操作,可以执行多次。
    • Stream.reduce:聚合操作,获取偶数并取总和
    • Stream.skip:获取数列,跳过数组的前三个数据元素
    • Stream.limit:获取数列,指定获取数组的前面三个数据元素
    • Stream.sort:对集合数据元素进行排序
    • Stream.sort and Stream.limit:演示limit和sort一起工作后,执行流水线的变化,注意有了limit之后,sort只对限定的数据集合进行排序。该演示可以看到Java Stream对中间操作的优化。
    • Stream.min and Stream.max:获取数列中的最大值和最小值
    • Stream.distinct:获取新数列,去除重复值
    • Stream.allMatch, Stream.anyMatch, Stream.noneMatch:判断数列中是否所有数字大于0,有数字大于0,没有任何数字大于0
    • Stream.iterate:获取等差数列
    • Stream.Collectors:对数列进行分组
  4. 演示Stream的串并行处理:StreamParallel
    • 当没有指定并行处理时,数列中的数据在主线程中一一执行peek操作。
    • 若指定了并行处理时,数列中的数据在不同线程中执行peek操作,执行顺序不定。

    如下为Stream串并行处理的演示输出,可以看到前者一一执行,后者有不同线程执行,顺序也不定,
    [20171124 22:16:09-970][main] 演示:串行执行数据流式处理
    [20171124 22:16:09-971][main] 0
    [20171124 22:16:09-971][main] 1
    [20171124 22:16:09-971][main] 2
    [20171124 22:16:09-971][main] 3
    [20171124 22:16:09-971][main] 4
    [20171124 22:16:09-971][main] 5
    [20171124 22:16:09-972][main] 6
    [20171124 22:16:09-972][main] 7
    [20171124 22:16:09-972][main] 8
    [20171124 22:16:09-972][main] 9
    [20171124 22:16:09-972][main] 演示:并行执行数据流式处理
    [20171124 22:16:09-975][main] 9
    [20171124 22:16:09-975][main] 8
    [20171124 22:16:09-976][main] 7
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 4
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 3
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 2
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 1
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 0
    [20171124 22:16:09-977][ForkJoinPool.commonPool-worker-2] 5
    [20171124 22:16:09-977][main] 6

4. Stream的生成

Stream对象主要有如下几种获取方式

  • Java Collection类
    • java.util.Collection.stream()
    • java.util.Collection.parallelStream()
  • Java Arrays类
    • Java.util.Arrays.stream()
  • 调用Java Stream中的静态工厂方法
    • java.util.stream.Stream.of(Object[])
    • java.util.stream.Stream.iterate()
    • java.util.stream.IntStream.range()
  • 自定义生成
    • java.util.StreamSupport
  • 其它
    • Random.ints()
    • BitSet.stream()

5. Stream操作流水线和操作方法分类

有了Stream集合数据,我们接下来就可以调用Stream提供的操作方法,对数据进行流式处理,这种流式处理也被称为操作流水线(Stream Pipeline)。

在了解操作流式线定义之前首先需要了解下操作方法,Java Stream类中提供每一种操作方法都可划分为如下两大类型,

  1. 中间操作(Intermediate Operation):会产生另外一个Stream对象
  2. 结束操作(Terminal Operation):产生结果

一个Stream流水线通常由一个数据源,N个中间操作,一个结束操作组成,

Stream Pipeline = Source + N * Intermediate operations + Terminal Operation

其中N>=0。

需要注意的是,所有的中间操作都是延后执行(Lazy Operation),换句话说,当Stream对象调用一个中间操作方法时,其操作不会立即执行,而是只有当Stream对象调用了结束操作方法时,才开始执行整个操作流水线,按顺序依次执行各种intermediate和terminal操作。

Stream Lazy Intermediate Operation这种特性能够让数据的处理效率得到提升,Java Stream可以分析中间操作的算法,进行相应的操作合并,或者根据结束条件,一旦有满足结束条件,则提前结束流水线执行。例如,在上述的filter-map-reduce样例中,可以让filtering\mapping\summing三次操作放入同一次循环中完成。再比如在一组数据集合中找出一个小于零的数字,那么只要找到第一小于零的数字则可以提前结束整个流水线操作。

中间操作的延后执行对于无限的数据集合是非常必要的。Stream在面对无限的数据集合时,必须要定义一个结束操作条件,一旦该条件满足了,则流水线执行结束。

中间操作可以分为无状态和有状态的操作,

  1. Stateless operation是指当前中间操作不会给后续操作带来状态,每个数据元素可以独立地执行后续操作并获取结果,各个数据元素在执行后续操作时互不影响,没有相互关联关系,filter/map就是一种stateless operations。
  2. Stateful operations是指只有所有的数据元素都执行完当前中间操作,才能获取到执行结果。Sort/Distinct就是一种有状态的操作,前者对数据集合进行排序,后者对数据进行重复值去除。

操作的状态有无对Stream流水线的并发执行有影响,如果是有状态的,在并发执行过程中会产生大量的数据需要缓存,而如果是无状态的,则数据的缓存量会大大减少。

还有一种是短路操作(short-circuiting),是指对一个无限的对象集合可能在有限的时间内产生一个有限的集合,短路操作能够提前结束对无限对象集合的流水线执行,比如limit/anyMatch操作,前者获取指定个数的对象集合,后者查询是否有匹配的数据元素。中间操作和结束操作都可能是short-circuiting。

下表列出java.util.stream.Stream中各操作方法所具有的属性分类,标识Y表明该方法具有当前属性,

Stream Operations Stateless Stateful Intermediate Terminal Short-Circuit
filter Y Y
map Y Y
mapToInt Y Y
mapToLong Y Y
mapToDouble Y Y
flatMap Y Y
flatMapToLong Y Y
flatMapToDouble Y Y
peek Y Y
distinct Y Y
sorted Y Y
skip Y Y
forEach Y
forEachOrdered Y
toArray Y
reduce Y
collect Y
min Y
max Y
count Y
anyMatch Y Y
allMatch Y Y
noneMatch Y Y
findFirst Y Y
findAny Y Y
limit Y Y Y

6. 参考资料

Java集合框架和各实现类性能测试

Java语言集合框架提供一系列集合接口类 (collection interface)和实现类,满足对集合中元素对象的各种集合抽象操作。

1. 集合接口类Collection/List/Set/Map

下图显示了在包java.util.*中主要的核心集合接口类,

上图中的Collection/List/Set/Map等都是泛型接口,各自的定义和作用如下:

  • Collection是集合的顶层接口定义,其它的集合类都继承于Collection(除Map),这个接口定义了对集合元素的增删改查,及其提供interator用于循环整个集合中的元素列表等等。
  • Set是一个不能有重复元素的集合。
  • List是一个有序元素集合,有序是指按照加入/插入数组位置的顺序,集合中可以有重复的元素,可以认为List就是一个数组,访问时可以通过位置index直接访问元素。
  • Map是对键值对(key/value)元素的集合,集合中不能有重复的key。
  • Queue/Deque是一个提供能够进行队列操作的集合,比如FIFO(先进先出)/ LIFO(后进先出)。
  • SortedSet/SortedMap是一个按照元素进行升序排列的集合,集合中的元素排序取决于Comparator的实现。

2. 集合实现类和工具类

集合的实现类很多,JDK中提供的实现类都在java.util.*包中,其中List/Set/Map有如下几个实现类,

  • List
    • ArrayList/LinkedList/Vector
  • Map
    • HashMap/LinkedHashMap/TreeMap/WeakHashMap
    • Hashtable/ConcurrentHashMap
  • Set
    • HashSet/LinkedHashSet/TreeSet

集合的工具类Collections/Arrays提供一些集合的复制、比较、排序和转换操作,

  • Utilities
    • Collections/Arrays

上述各个实现类和接口类的关系见下图,

3. 集合实现类的特点和数据结构

下面以表格形式列出各个实现类的特点和区别,方便进行对比,其中不同的数据结构决定了各个集合实现类的不同性能特点,详细的数据结构描述见后面注解。

集合接口 集合实现类 是否按插入顺序存放 是否按有序存放(1) 是否可以存放重复元素 是否线程安全 数据结构特性描述
List ArrayList Yes No Yes No 基于动态数组的数据结构,注2
LinkedList Yes No Yes No 基于双向链表的数据结构,查询慢,插入快,注3。
Vector Yes No Yes Yes* Deprecated,注4。
Map HashMap No No Yes No 基于哈希表的元素数据离散分布,注5。
LinkedHashMap No No Yes No 基于哈希表的元素数据离散分布,除此之外集合元素数据之间有双向链表指针,注6。
TreeMap No Yes Yes No 基于红黑树的数据结构,元素需要提供Comparator实现,用于有序存放,注7。
WeakHashMap No No Yes No
Hashtable No No Yes Yes 基于哈希表的元素数据分散分布,通过对象锁实现线程安全
ConcurrentHashMap No No Yes Yes 通过lock实现线程安全,在多线程环境中比Hashtable有更好的并发性能
Set HashSet No No No No 底层使用HashMap实现
LinkedHashSet Yes No No No 底层使用LinkedHashMap实现
TreeSet No Yes No No 底层使用TreeMap实现,元素需要提供Comparator实现,用于有序存放

注1:元素是否按有序存放,是指集合中元素根据Comparator进行比较后升序排列。

注2:ArrayList是基于动态数组的数据结构,数据插入时,会导致整个后端数据的往后移动一位,所以插入速度慢,但是根据位置索引可以直接访问元素数据,所以通过位置索引查询数据速度会很快。

注3:LinkedList是基于双向链表的数据结构,插入快,但是查询会比较慢。另外LinkedList支持getFirst/getLast/removeFirst/removeLast/addFirst/addLast操作,可以方便实现FIFO/LIFO队列操作。双向链表的数据结构如下图所示,

注4:Vector由于其在所有的get/set上进行了synchronize,导致难于在并发编程发挥作用,在很多时候可以使用List list = Collections.synchronizedList(new ArrayList())方法取代,目前不建议使用Vector来用于线程安全的编程。

注5:HashMap基于哈希表的元素数据离散分布,是指数据按照一定规则进行离散,比如数值按16取模,各自落入不同的子集合,因此数据元素排列插入后无序,见下图所示,

注6:LinkedHashMap在集合元素数据之间有双向链表指针,数据的删除和插入快,数据元素排列后无序,见下图所示,

注7:TreeMap基于红黑树(近平衡二叉树)的数据结构,这个数据结构最大的特点是各个元素数据达到平衡分布,最远和最近叶子节点离根节点距离相差不超1,使得元素的查找和插入速度达到O(log N)级别。

4. 集合实现类的插入操作

我们可以尝试为各个集合实现类进行插入数据操作,然后查看数据元素在集合中的数据排列,下面主要观察数据排列是否有序,是否按照插入顺序排列。通过观察,可以更加深入地了解各个实现类的数据结构和特性。

List的数据插入

下面的代码新建了三个List实现类实例,然后依次插入10个随机数,最后打印出列表数据。

List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new LinkedList<>();
List<Integer> list3 = new Vector<>();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入List中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 list1.add(value);
 list2.add(value);
 list3.add(value);
}

App.logMessage("ArrayList:" + list1);
App.logMessage("LinkedList:" + list2);
App.logMessage("Vector:" + list3);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:41
第 1 次产生的随机数为:68
第 2 次产生的随机数为:62
第 3 次产生的随机数为:4
第 4 次产生的随机数为:18
第 5 次产生的随机数为:38
第 6 次产生的随机数为:97
第 7 次产生的随机数为:9
第 8 次产生的随机数为:19
第 9 次产生的随机数为:1
ArrayList:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]
LinkedList:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]
Vector:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]

可以看到,各个List的数据元素排列和插入顺序一致,这也是由于动态数组的数据结构带来的特性。

Set的数据插入

下面的代码新建了三个Set实现类实例,然后依次插入10个随机数,最后打印出列表数据。

Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new LinkedHashSet<>();
Set<Integer> set3 = new TreeSet<>();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入Set中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 set1.add(value);
 set2.add(value);
 set3.add(value);
}

App.logMessage("HashSet:" + set1);
App.logMessage("LinkedHashSet:" + set2);
App.logMessage("TreeSet :" + set3);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:51
第 1 次产生的随机数为:86
第 2 次产生的随机数为:24
第 3 次产生的随机数为:66
第 4 次产生的随机数为:76
第 5 次产生的随机数为:59
第 6 次产生的随机数为:13
第 7 次产生的随机数为:34
第 8 次产生的随机数为:89
第 9 次产生的随机数为:21
HashSet:[66, 34, 51, 21, 86, 24, 89, 59, 76, 13]
LinkedHashSet:[51, 86, 24, 66, 76, 59, 13, 34, 89, 21]
TreeSet :[13, 21, 24, 34, 51, 59, 66, 76, 86, 89]

可以看到,HashSet/LinkedHashSet无序,TreeSet按大小依次排列。这是由于HashSet/LinkedHashSet/TreeSet底层实现用的是HashMap/LinkedHashMap/TreeMap,因此继承了各自的特点。

Map的数据插入

下面的代码新建了五个Map实现类实例,然后依次插入10个随机数(随机数为key),最后打印出列表数据。

Map map1 = new HashMap();
Map map2 = new TreeMap();
Map map3 = new LinkedHashMap();
Map map4 = new WeakHashMap();
Map map5 = new Hashtable();
Map map6 = new ConcurrentHashMap();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入map中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 if (!map1.containsKey(value)){
 map1.put(value, i);
 map2.put(value, i);
 map3.put(value, i);
 map4.put(value, i);
 map5.put(value, i);
 map6.put(value, i);
 }
}

App.logMessage("产生的随机数为key,index为value");
App.logMessage("HashMap:" + map1);
App.logMessage("TreeMap:" + map2);
App.logMessage("LinkedHashMap:" + map3);
App.logMessage("WeakHashMap:" + map4);
App.logMessage("Hashtable:" + map5);
App.logMessage("ConcurrentHashMap:" + map5);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:48
第 1 次产生的随机数为:86
第 2 次产生的随机数为:81
第 3 次产生的随机数为:19
第 4 次产生的随机数为:90
第 5 次产生的随机数为:74
第 6 次产生的随机数为:55
第 7 次产生的随机数为:29
第 8 次产生的随机数为:89
第 9 次产生的随机数为:65
产生的随机数为key,index为value
HashMap:{48=0, 81=2, 65=9, 19=3, 86=1, 55=6, 89=8, 90=4, 74=5, 29=7}
TreeMap:{19=3, 29=7, 48=0, 55=6, 65=9, 74=5, 81=2, 86=1, 89=8, 90=4}
LinkedHashMap:{48=0, 86=1, 81=2, 19=3, 90=4, 74=5, 55=6, 29=7, 89=8, 65=9}
WeakHashMap:{90=4, 74=5, 89=8, 29=7, 65=9, 55=6, 81=2, 86=1, 48=0, 19=3}
Hashtable:{90=4, 89=8, 65=9, 19=3, 86=1, 81=2, 55=6, 29=7, 74=5, 48=0}
ConcurrentHashMap:{90=4, 89=8, 65=9, 19=3, 86=1, 81=2, 55=6, 29=7, 74=5, 48=0}

可以看到,TreeMap按key大小升序排列,LinkedHashMap按value大小升序排列,其它无序。

集合实现类的性能比较

为了比较各个集合实现类的性能,对各个集合实现类进行如下三个操作,

  • 插入:在集合中插入0-N个数据元素,N为指定性能测试要达到的数组大小
  • 查询:在集合中一一查询0-N个数据元素,N为指定性能测试的数组大小,换句话说轮询集合中所有元素
  • 删除:在集合中一一删除0-N个数据元素,N为指定性能测试的数组大小,换句话说删除集合中所有元素

测试数组大小分别为1万、5万、10万、15万,线性倍增,然后观察各个集合实现类在不同数组大小下的性能表现。

注:在Map中查询分别添加了通过key/value查询的测试。

测试环境:Java版本为1.8.0_111,主机环境:Win7 SP1 x64, intel i5-6600K 4 cores 3.5GHz CPU, 16G memory。

可以看到,

  • List集合实现类在插入、查询、删除操作上,随着数组变大有明显的性能开销增加,性能开销的增加倍数超过数组大小的增加倍数。
  • Map集合实现类在通过value查询性能开销很大,在实际编程中,尽量避免此类操作
  • 表中有些操作时间开销都在10毫秒内,随着数组倍增,性能表现不错,这些操作可以在编程中多加利用。

测试程序代码

演示代码仓库地址:https://gitee.com/pphh/blog,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/blog.git

上述测试程序代码样例在文件路径171117_java_collection\demo中。

参考资料