浅析Java 8中的集合遍历
2014-10-22钱宇虹
摘 要:Java平台提供了多种方式遍历对象的集合,其中包括今年3月19日发布的Java 8中引入的新特性。本文回顾了迭代器,着重分析了主动式迭代器和被动式迭代器之间的差异,研究了Java 8的foreach()方法和Stream API如何改进和并行化Java迭代器的行为,然后对主动迭代、流和并行流这三种方法进行了性能比较。总之,Java 8的迭代器可读性更好,不易出错,也更容易并行化。
关键词:Java8;集合;迭代器
中图分类号:TP311 文献标识码:A
1 引言(Introduction)
在编程世界里一般需要提供一种机制遍历软件对象的集合。大多数编程语言都有类似于数组的功能并且直接支持数组元素的遍历,但是现代的编程语言还支持更为复杂的数据结构,如列表、集合、映射和树,遍历能力是通过公共方法提供,而内部细节都隐藏在类的私有部分,所以程序员不需要了解其内部实现就能够遍历这些数据结构中的元素,这就是迭代的目的。
迭代器是对集合中的所有元素进行顺序访问并可以对每个元素执行某些操作的机制。迭代器在本质上提供了在封装的对象集合上做“循环”的装置。常见的使用迭代器的例子有:
访问目录中的每个文件并显示文件名;访问队列中的每个客户(如银行排队)并判断他或她等待了多久。使用迭代器时,一般情况下可以循环嵌套,即可以在同一时间做多个遍历;迭代器应该是无损的,即迭代行为不应该改变集合本身,如迭代时不要从集合中移除或插入元素;在某些情况下你还需要使用迭代器的不同的遍历方法,例如,树的前序遍历和后序遍历,或者深度优先和广度优先遍历。
根据Gang of Four,迭代器设计模式是一种行为模式,其核心思想是负责访问和遍历列表中的对象,并把这些对象放到一个迭代器对象中[1]。迭代器的实现方法根据谁来控制迭代分为两种:主动迭代和被动迭代。主动迭代器是由客户程序创建迭代器,调用next()行进到下一个元素,测试查看是否所有元素已被访问等等,总之客户程序是可以操作的。这种方法在象C++这样的语言中最为常见,在GoF的书中也是最为关注的方法,主动迭代器在Java 8之前可以说是Java的唯一选择。被动迭代器则是迭代器本身控制迭代,即迭代器自行next()向下走,针对客户程序来说迭代是透明的,是不能操作的。这种方法在象LISP这样的语言中很常见。随着Java 8的发布,这种迭代方法也成为Java程序员的一个选择。
2 Java 8之前的迭代(Iteration before Java 8)
为了说明Java中的各种迭代方法,我们需要一个集合并对集合中的元素做些操作,本文选择代表事物或人物名称的字符串的集合,我们将简单地打印集合中的每个名称到标准输出。这些基本的思想很容易扩展到更为复杂的对象的集合(如员工),并在处理每一个对象的时候可以涉及更为复杂的操作。
在Java 1.0和1.1中两个主要的集合类是Vector(向量)和Hashtable(哈希表),迭代器是通过一个叫做Enumeration(枚举)的类实现的。今天无论是Vector还是Hashtable都是泛型类,但退回到那时泛型还不是Java语言的一部分。代码清单1演示了使用枚举来处理字符串向量的方法。
Vector names = new Vector();
names.add("Apple");
names.add("Orange");
Enumeration e = names.elements();
while (e.hasMoreElements())
{
String name = (String) e.nextElement();
System.out.println(name);
}
代码清单1:使用枚举处理字符串向量
List names = new LinkedList();
names.add("Apple");
names.add("Orange");
Iterator i = names.iterator();
while (i.hasNext())
{
String name = (String) i.next();
System.out.println(name);
}
代码清单2:使用迭代器处理字符串列表
Java 1.2推出了集合类(Collections),并通过一个迭代器类(Iterator)实现了迭代器设计模式。因为当时在Java 1.2中还没有泛型,所以对迭代器返回的对象进行强制类型转换仍然是必要的。对于Java版本1.2至1.4,遍历字符串列表如代码清单2所示。
Java 5给出了泛型、Iterable接口和增强for循环。在增强for循环中,迭代器的创建和调用它的hasNext()和next()方法都发生在幕后,不需要明确地写在代码中,因此代码显得更为紧凑。使用Java 5,我们的例子类似代码清单3所示,请注意,在Java 5我们使用的仍然是主动迭代器。
List
LinkedList
names.add("Apple");
names.add("Orange");
for (String name : names)
System.out.println(name);
代码清单3:使用泛型和增强for循环
List
names.add("Apple");
names.add("Orange");
names.forEach(name-> System.out.println(name));
代码清单4:Java8使用forEach()方法进行迭代
Java 7为了避免泛型的冗长给出了钻石运算符<>,从而避免了使用new运算符实例化泛型类时重复指定数据类型。从Java 7开始,代码清单3中的第一行可以简化成以下形式:List
3 Java 8中的迭代(Iteration in Java 8)
3.1 forEach()方法
Java8为我们提供了新的迭代途径,它使用lambda表达式开展集合的遍历。Java8最主要的新特性就是lambda表达式以及与此相关的特性,如流(streams),方法引用(method references)和功能接口(functional interfaces)。正是因为这些新特性使我们能够使用被动迭代器而不是传统的主动迭代器,特别是Iterable接口提供了一个被动迭代器的缺省方法(default method)叫做forEach()。缺省方法是Java 8的又一个新特性,是一个接口方法的缺省实现,在这种情况下,forEach()方法实际上是用类似于代码清单3的主动迭代器方式来实现的。
实现了Iterable接口的集合类(如:所有列表List、集合set)现在都有一个forEach()方法,这个方法接收一个功能接口参数,实际上传递给forEach()方法的参数是一个lambda表达式。使用Java 8的功能,我们的例子将演变成代码清单4中所示的形式。
请注意清单4中的被动迭代与前面三个清单中的主动迭代之间的差异。在主动迭代中由循环结构控制迭代,并且每次通过循环从列表中获取一个对象,然后打印出来。而在清单4中没有显式的循环结构,我们只是告诉forEach()方法对列表中的对象实施打印,迭代控制隐含在forEach()方法中。
3.2 流
如果要做一些比打印名字稍微复杂一点的事情,比如,计算以字母A开头的人名的个数,就需要用lambda表达式,或者使用Java 8的流API(Stream API)来实现更复杂的逻辑了。
流是应用在一组元素上的一次执行的操作序列。集合和数组都可以用来产生流,因此它们称作数据源,但流不存储集合中的元素,相反,流是通过管道(pipeline)操作来自数据源的值序列的一种机制。流管道(Stream Pipeline)由数据源(Stream Source)、若干中间操作(Intermediate Operations)和一个最终操作(Terminal Operation)组成,中间操作对数据集完成过滤、检索等中间业务,而最终操作完成对数据集处理的最终结果,或者调用forEach()方法。
List
names.add("Annie");
names.add("Alice");
names.add("Bob");
long count = names.stream()
.filter(name -> name.startsWith("A")
.count();
代码清单5:Java8使用流管道计算以字母A开头的人名的
个数
List
names.add("Annie");
names.add("Alice");
names.add("Bob");
long count = 0;
for (String name : names){
if (name.startsWith("A"))
++count;
}
代码清单6:Java 7使用主动迭代计算以字母A开头的人
名的个数
如清单5所示,列表names用于创建流,然后使用过滤器对数据集进行过滤,filter()方法只过滤出以字母A开头的名字,该方法的参数是一个lambda表达式[2]。最后,流的count()方法作为最终操作,得到应用结果。
中间操作除了filter()之外,还有distinct()、sorted()、map()等等,其一般是对数据集的整理(过滤、排序、匹配、抽取等等),返回值一般也是数据集。
最终方法往往是完成对数据集中数据的处理,如forEach(),还有allMatch()、anyMatch()、findAny()、findFirst(),数值计算类的方法有sum()、max()、min()、averag()等等。最终方法也可以是对集合的处理,如reduce()、collect()等等。reduce()方法的处理方式一般是每次都产生新的数据集,而collect()方法是在原数据集的基础上进行更新,过程中不产生新的数据集。流管道操作更为详细的资料请参阅Java Tutorial[3]。
为了理解Java8的流的重要性,请比较代码清单5和代码清单6。代码清单6是大多数Java开发人员比较熟悉的,它使用主动式迭代完成同样的功能。很显然,只要掌握了流的基本概念和操作,清单5中的方法易读性更好且不易出错,特别是,在多线程环境下清单6中的逻辑不是线程安全的,而清单5是线程安全的,它也更容易进行并行化。
Java8集合类不仅具有Stream()方法,该方法返回一个连续的数据流,Java8集合类还有一个parallelStream()方法,该方法返回一个并行流。并行流的作用在于允许管道操作同时在不同的Java线程中执行以提高性能;但要注意,集合元素的处理顺序可能发生改变。测试显示,使用并行流打印列表中的名字,他们打印的顺序不同于他们在列表中出现的顺序。下面是使用并行流之后的代码:
long count=names.parallelStream().filter(name-> name.startsWith("A")).count();
4 性能比较(Performance comparison)
虽然使用Java 8中的被动迭代器有多种优势,那么它是否提供更快的集合处理速度呢?为了比较主动迭代器、流和并行流的性能,下面我们还是以计算字母A开头的人名的个数为例,采取上述三种迭代方法做一个比较测试。在一台双核奔腾处理器、4G内存、64位Windows 7和64位版本的Java 8配置环境下,使用五种不同的集合类(LinkedList,ArrayList、HashSet、LinkedHashSet和TreeSet)运行上述程序,将平均时间总结在如下所示的表1中。
5 结论(Conclusion)
Java 8支持一种新功能进行迭代,它是声明性的方法,这种新方法带来的好处是代码的可读性更好,不易出错,也更容易并行化。然而测试表明,并行流对每一种集合类而言不一定性能更快。
参考文献(References)
[1] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides,Design Patterns: Elements of Reusable Object-OrientedSoftware [A], Addison-Wesley Professional, 1994.
[2] Oracle, The JavaTM Tutorials,Lambda Expressions [EB/OL],http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html.
[3] Oracle, The JavaTM Tutorials,Lesson: Aggregate Operations[EB/OL],http://docs.oracle.com/javase/tutorial/collections/streams/index.html.
作者简介:
钱宇虹(1967-),女,硕士,副教授.研究领域:软件开发与应用,软件工程,软件测试技术.endprint
Java8集合类不仅具有Stream()方法,该方法返回一个连续的数据流,Java8集合类还有一个parallelStream()方法,该方法返回一个并行流。并行流的作用在于允许管道操作同时在不同的Java线程中执行以提高性能;但要注意,集合元素的处理顺序可能发生改变。测试显示,使用并行流打印列表中的名字,他们打印的顺序不同于他们在列表中出现的顺序。下面是使用并行流之后的代码:
long count=names.parallelStream().filter(name-> name.startsWith("A")).count();
4 性能比较(Performance comparison)
虽然使用Java 8中的被动迭代器有多种优势,那么它是否提供更快的集合处理速度呢?为了比较主动迭代器、流和并行流的性能,下面我们还是以计算字母A开头的人名的个数为例,采取上述三种迭代方法做一个比较测试。在一台双核奔腾处理器、4G内存、64位Windows 7和64位版本的Java 8配置环境下,使用五种不同的集合类(LinkedList,ArrayList、HashSet、LinkedHashSet和TreeSet)运行上述程序,将平均时间总结在如下所示的表1中。
5 结论(Conclusion)
Java 8支持一种新功能进行迭代,它是声明性的方法,这种新方法带来的好处是代码的可读性更好,不易出错,也更容易并行化。然而测试表明,并行流对每一种集合类而言不一定性能更快。
参考文献(References)
[1] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides,Design Patterns: Elements of Reusable Object-OrientedSoftware [A], Addison-Wesley Professional, 1994.
[2] Oracle, The JavaTM Tutorials,Lambda Expressions [EB/OL],http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html.
[3] Oracle, The JavaTM Tutorials,Lesson: Aggregate Operations[EB/OL],http://docs.oracle.com/javase/tutorial/collections/streams/index.html.
作者简介:
钱宇虹(1967-),女,硕士,副教授.研究领域:软件开发与应用,软件工程,软件测试技术.endprint
Java8集合类不仅具有Stream()方法,该方法返回一个连续的数据流,Java8集合类还有一个parallelStream()方法,该方法返回一个并行流。并行流的作用在于允许管道操作同时在不同的Java线程中执行以提高性能;但要注意,集合元素的处理顺序可能发生改变。测试显示,使用并行流打印列表中的名字,他们打印的顺序不同于他们在列表中出现的顺序。下面是使用并行流之后的代码:
long count=names.parallelStream().filter(name-> name.startsWith("A")).count();
4 性能比较(Performance comparison)
虽然使用Java 8中的被动迭代器有多种优势,那么它是否提供更快的集合处理速度呢?为了比较主动迭代器、流和并行流的性能,下面我们还是以计算字母A开头的人名的个数为例,采取上述三种迭代方法做一个比较测试。在一台双核奔腾处理器、4G内存、64位Windows 7和64位版本的Java 8配置环境下,使用五种不同的集合类(LinkedList,ArrayList、HashSet、LinkedHashSet和TreeSet)运行上述程序,将平均时间总结在如下所示的表1中。
5 结论(Conclusion)
Java 8支持一种新功能进行迭代,它是声明性的方法,这种新方法带来的好处是代码的可读性更好,不易出错,也更容易并行化。然而测试表明,并行流对每一种集合类而言不一定性能更快。
参考文献(References)
[1] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides,Design Patterns: Elements of Reusable Object-OrientedSoftware [A], Addison-Wesley Professional, 1994.
[2] Oracle, The JavaTM Tutorials,Lambda Expressions [EB/OL],http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html.
[3] Oracle, The JavaTM Tutorials,Lesson: Aggregate Operations[EB/OL],http://docs.oracle.com/javase/tutorial/collections/streams/index.html.
作者简介:
钱宇虹(1967-),女,硕士,副教授.研究领域:软件开发与应用,软件工程,软件测试技术.endprint