jungleford如是说
对于Java集合框架(Java Collections Framework,JCF),Java玩家大概都不会陌生,在C++里面相似的概念是标准模板库(Standard Template Library,STL),主要是对一些数据结构和相关算法的封装。考虑到这是一个Java初学者将会经常接触的工具,所以有了以下的一些文字。主要是参考了IBM developerWorks上的一篇教程,它可能解释得更加清晰,这里算是浓缩了一下吧,真正的来龙去脉可以看看JDK文档里的“The Collections Framework”,说明更为详细。
问题的源头
JCF的全家福
好啦好啦,这些我们都知道,又不是在上数学课,说了这么多废话,怎么还没扯到正题上来?JCF的影子我还没看见呢!列位看官别急,这就给您道来。其实上面的概念对理解JCF非常重要。
JCF是个颇有点规模的家族,看看它的类层次关系图就知道了,下面这张图(图1)摘自著名的Thinking in Java:
图1
哇,这么多接口和类,真有点让人无从下手的感觉。其实我们真正需要记住的只是这么一个超超easy的结构(图2):
图2
这张图看起来舒服多了吧?但它又能说明什么问题呢?它怎么就能够把握整个JCF呢?我们把Collection接口置于最顶上,意思是想说:Collection其实是整个JCF家族中的“祖宗”,几乎所有的JCF成员都源自该接口,或者和它有密切的关系,Collection提供关于集合的一些通用操作的接口,包括插入(add()方法)、删除(remove()方法)、判断一个元素是不是其成员(contains()方法)、遍历(iterator()方法)等等。注意了,前面的“废话”在这里将得到体现:Set接口体现的是“无序集”的概念,它是不允许有重复元素出现的;List接口代表“有序集”;而Map接口则是“映射”(在早期的Java版本中并不叫Map,我们在后面会看到),其实Map.Entry接口就是代表一个“元素对”我们可以通过Map的entrySet()方法得到这样一个由“元素对”组成的Set对象。我们注意到Set和List都是从“祖宗”Collection派生的,而Map不是,毕竟对一对元素的操作与对单个元素的操作还是有区别的,但是如果你仔细对照一下Collection和Map的源代码,以及它们的直接后代AbstractCollection和AbstractMap的源代码,你将会发现很多相似的地方,所以我们仍然可以把Map看成是和Collection有着血缘关系的接口,而和Set,List一起处于并列的位置。
有了“无序集”,“有序集”和“映射”,我们就可以定义各种各样的抽象数据结构了,譬如图1所示的向量,链表,堆栈,哈希表,平衡二叉树等。但我们需要记住的,仅仅是图2,置于其它的成员,在用到的时候查一下API手册不就行了?不过一般初学者还是比较容易用到一些类,像Vector、ArrayList、HashMap,我在这里列了一张表,显示了常见的JCF成员及其关系:
可能有的概念您还不是太了解,譬如什么叫“历史集合”,Hashtable、HashMap、TreeMap三者之间有什么区别和联系,怎样实现对一个特定集合的快速遍历、元素查找或者排序,没关系,我们在下面将逐一进行研究。
细节考虑:目标与效率
有了JCF的层次还不够,重要的是对集合所容纳的对象的具体操作,以前我们学数据结构的时候可能老师总会让你计算一个算法的时间复杂度,可能你会对这个O(f(n))很不耐烦,但事实上算法效率是一个重要的因素。
对集合的有两个主要的应用:我需要知道集合有哪些元素;根据条件找到一个特定的元素。在算法上通常称为“遍历”和“查找”。不要以为我们生活中不常用哦!譬如CCTV的“幸运52”里面,李咏让参赛者报出一款PDA的准确价位,他会怎么做?“2000”“高了”“1000”“低了”“1500”“低了”……直到答对为止。可能有很多人都会选择这个策略,无论他是不是计算机专业出身的,也不知道他是不是了解“数据结构”和“折半查找”,更不用说他是不是知道还有比在无初始代价下O(log n)的时间复杂度更快的算法了,但我们经常会自然而然地用这样的方法,这和一个人的行业无关,除非这个人的rp超强,呵呵……
又讲了一堆题外话了,遍历和修改似乎是一对矛盾,一个可以高效率插入删除元素的数据结构通常遍历的性能并不是最优。于是JCF在这里根据用户的目标实现了两种定制的数据结构:哈希表(包括HashSet和HashMap)和平衡二叉树(包括TreeSet和TreeMap)。由于可排序性是一种独特的要求,所以引入了SortedSet和SortedMap,它们分别是AbstractSet和AbstractMap的子接口,而TreeSet和TreeMap又分别是他们的一种实现。熟悉数据结构的人可能比较了解,哈希表在进行插入、删除、查找这样的操作是很快的,其时间复杂度是常数级O(1);平衡二叉树虽然插入、删除操作比较麻烦(需要O(log n)的代价),但进行遍历和排序却很快。选择完全在于用户的侧重点,但由于类型转换的方便性,通常我们用哈希表构造一个集合以后,再把它转换成相应的树集进行遍历,以获得较好的效果。
Set set1 = new HashSet();
set1.add(elem1);// 通过插入元素构造集合
set1.add(elem2);
set1.add(elem3);
Set set2 = new TreeSet(set);
Iterator all = set2.iterator();
while (all.hasNext())
{// 遍历集合
all.next();
...
}
历史实现(Legacy Implementations)是JCF的一个术语,准确的意义不是很清楚,但大致可以认为在Java 2(JDK 1.2)出现以前的老版本中JCF的一个雏形框架。在Java 2以后,JCF才开始完善健壮起来,新实现中出现了一些新的类用于替代老版本中的成员,但由于种种原因,老版本中很多类都代表了传统数据结构的精髓部分,以及一些安全原因,所以仍然被我们使用着。
Enumeration vs. Iterator
Enumeration是一个传统的集合遍历工具,在新的JCF中使用的是Iterator,Iterator同样具有遍历功能,还包含一个remove()方法来删除当前得到的元素。
Dictionary vs. Map
Dictionary是一个现在已经被标记为deprecated的类,实现了老版本中的映射功能,现在已经完全被Map取代。它们的区别是:Dictionary不key和value不能为null,但Map却允许空的关键字和值,这一点直接影响到它们的后代:Hashtable和HashMap。
Vector vs. ArrayList
Vector和ArrayList是数组在JCF中的体现,还记得前面讲过的数组的缺点么?Vector和ArrayList就是一种可以动态增长的数组。Vector是历史实现,它和ArrayList的主要区别在于,Vector是同步集合(或者说是线程安全的),但ArrayList并不是同步的,由于同步需要花一定的代价,所以ArrayList看起来要比Vector的存取访问效率更高。关于同步我们下面还将要谈到。
Hashtable vs. HashMap
Hashtable是Dictionary的子类,属于历史实现,而HashMap是Map的子类,是新实现。它们的区别除了上面所说的key和value是否可以为空之外,也有同步的差别,Hashtable是同步的,但HashMap不是。不过不要因为Hashtable是“老前辈”而瞧不起它哦,它的一个著名的子类Properties我们可是经常会用到的。
从上面的描述中我们似乎可以得出这么一个印象:历史实现好像都是同步的,但新实现中却没有。需要同步操作的理由是,可能存在多个线程对同一个集合进行操作的情况:譬如一个线程正在对某集合进行遍历,但与此同时,另一个线程又在对该集合进行插入或删除,那么第一个线程的遍历结果将是不可预测的,对于同步集合,它将会抛出一个ConcurrentModificationException异常,JCF把这种机制成为“fail-fast”。我们对比一下Vector和ArrayList的源代码就可以发现Vector的很多方法都是有synchronized关键字修饰的,但ArrayList没有。
4. 容易遗忘的工具: Collections 和 Arrays在图1中右下角落里有两个类叫做Collections(注意,不是Collection!)和Arrays,这是JCF里面功能强大的工具,但初学者往往会忽视。按JCF文档的说法,这两个类提供了封装器实现(Wrapper Implementations)、数据结构算法和数组相关的应用。
想必大家不会忘记上面谈到的“折半查找”、“排序”等经典算法吧,Collections类提供了丰富的静态方法帮助我们轻松完成这些在数据结构课上烦人的工作:
binarySearch:折半查找。
sort:排序,这里是一种类似于快速排序的方法,效率仍然是O(n * log n),但却是一种稳定的排序方法。
reverse:将线性表进行逆序操作,这个可是从前数据结构的经典考题哦!
rotate:以某个元素为轴心将线性表“旋转”——哇,这个功能太酷了!
swap:交换一个线性表中两个元素的位置。
……
Collections还有一个重要功能就是“封装器”(Wrapper),它提供了一些方法可以把一个集合转换成一个特殊的集合:
unmodifiableXXX:转换成只读集合,这里XXX代表六种基本集合接口:Collection、List、Map、Set、SortedMap和SortedSet。如果你对只读集合进行插入删除操作,将会抛出UnsupportedOperationException异常。
synchronizedXXX:转换成同步集合。
singleton:创建一个仅有一个元素的集合,这里singleton生成的是单元素Set,singletonList和singletonMap分别生成单元素的List和Map。
空集:由Collections的静态属性EMPTY_SET、EMPTY_LIST和EMPTY_MAP表示。
此外,我们知道把集合转换成对象数组可以用Collection的toArray()方法,我们也可以方便地把一个对象数组转换成一个线性表(可不要告诉我你是一个一个地add哦):Arrays.asList()。
目前我们了解的JCF的一个重要特征是:所有加入到集合当中的对象都将在表面上失去它们自己的特性,而看上去仅仅只是一个Object对象而已,除非你把它强制类型转换成它们原来的对象。这一点很自然,集合嘛,对象的容器,它容纳的是各种各样的对象,而不仅仅是某种特定类型的对象。J2SE 5.0出现以后,JCF开始引入泛型的特性,譬如我们经常碰到这样的应用,就是把集合转换成特定的数组,虽然Collection有toArray()的方法,但可惜的是,这个数组的所有元素都是Object类型的,我们通常的做法是用一个for循环把数组的每个元素都进行强制类型转换,虽然可行,但看上去很笨拙,如果有了泛型,我们就可以预先指定要得到的类型,然后一次toArray就可以得到我们期望的数组,里面的元素全部都是指定类型了。惭愧的是,我对5.0还不是太了解,具体可以参考J2SE 5.0的JCF文档。
小结
我这里走马观花一样把JCF的一些主要概念罗嗦了一下,Java的老手们可能比较厌烦,新手们可能更觉得像回顾了一下高中的数学课和大学的数据结构,呵呵。这只是一个小小的例子,可见基础知识对现实当中的应用还是蛮有指导意义的。大师们看数学,觉得那是很唯美很艺术的一样东西,西方一直都把数学区别于其它自然科学,而认为它更靠近于哲学,像我等这样整天还在为找工作烦得要死的俗人还是没法入道啊,sigh……