当前位置:   article > 正文

Java集合框架(JCF:Java Collections Framework)之概述

Java集合框架(JCF:Java Collections Framework)之概述
一、集合论引述

    
集合论是现代数学中重要的基础理论。它的概念和方法已经渗透到代数、拓扑和分析等许多数学分支以及物理学和质点力学等一些自然科学部门,为这些学科提供了奠基的方法,改变了这些学科的面貌。计算机科学作为一门现代科学因其与数学的缘源,自然其中的许多概念也来自数学,集合是其中之一。如果说集合论的产生给数学注入了新的生机与活力,那么计算机科学中的集合概念给程序员的生活也注入了新的生机与活力。
1
、什么是集合
很难给集合下一个精确的定义,通常情况下,把具有相同性质的一类东西,汇聚成一个整体,就可以称为集合。比如,用 Java 编程的所有程序员,全体中国人等。通常集合有两种表示法,一种是列举法,比如集合 A ={ 1,2,3,4 },另一种是性质描述法,比如集合 B= X|0<X<100 X 属于整数}。集合论的奠基人康托尔在创建集合理论给出了许多公理和性质,这都成为后来集合在其它领域应用的基础,本文并不是讲述集合论的,所以如果你对集合论感兴趣,可以参考相关书籍。
2、什么是集合框架
那么有了集合的概念,什么是集合框架呢?集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。
·   接口 :即表示集合的抽象数据类型。接口提供了让我们对集合中所表示的内容进行单独操作的可能。
·   实现 :也就是集合框架中接口的具体实现。实际它们就是那些可复用的数据结构。
·   算法 :在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。
如果你学过 C++ ,那 C++ 中的标准模版库( STL )你应该不陌生,它是众所周知的集合框架的绝好例子。
3
、集合框架对我们编程有何助益
到底集合框架对我们编程有什么好处呢?
·   它减少了程序设计的辛劳。集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。通过这些在无关 API 之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些 API 而去写大量的代码。
·   它提高了程序速度和质量。集合框架通过提供对有用的数据结构和算法的高性能和高质量的实现使你的程序速度和质量得到提高。因为每个接口的实现是可互换的,所以你的程序可以很容易的通过改变一个实现而进行调整。另外,你将可以从写你自己的数据结构的苦差事中解脱出来,从而有更多时间关注于程序其它部分的质量和性能。
·   减少去学习和使用新的 API  的辛劳。许多 API 天生的有对集合的存储和获取。在过去,这样的 API 都有一些子 API 帮助操纵它的集合内容,因此在那些特殊的子 API 之间就会缺乏一致性,你也不得不从零开始学习,并且在使用时也很容易犯错。而标准集合框架接口的出现使这个问题迎刃而解。
·   减少了设计新 API 的努力。设计者和实现者不用再在每次创建一种依赖于集合内容的 API 时重新设计,他们只要使用标准集合框架的接口即可。
·   集合框架鼓励软件的复用。对于遵照标准集合框架接口的新的数据结构天生即是可复用的。同样对于操作一个实现了这些接口的对象的算法也是如此。
有了这些优点,并通过合理的使用,它就会成为程序员的一种强大的工具。不过,从历史上来看,集合大多其结构相当复杂,也就给它们一个造成极不合理的学习曲线的坏名声。但是,希望 Java2 的集合框架能缩短你的学习曲线,从而快速掌握它。
在许多高级语言中的数组其实也是集合的一种简单实现,比如 C,C++,Pascal Java 。在 C 中的数组的一种可能结构如下图所示:

数组保存着相同类型的多个值,它的长度在数组被创建时就固定下来,建立之后就无法改变。如果你需要一种大小能动态改变的存储结构,数组就不适合了,这时集合框架就有了用武之地了。
二、 Java1.2 之前的容器类库

其实在 Java2 之前, Java 是没有完整的集合框架的。它只有一些简单的可以自扩展的容器类,比如 Vector Stack Hashtable 等。 Vector 中包含的元素可以通过一个整型的索引值取得,它的大小可以在添加或移除元素时自动增加或缩小。然而, Vector 的设计却存在极多缺限(下面会说到)。 Stack 是一种后进先出( LIFO )的堆栈序列,学过数据结构的都会知道,它的重要特点是先放入的东西最后才能被取出。 Hashtable Java2 中的 Map 类似,可以看成一种关联或映射数组,可以将两个或多个毫无关系的对象相关联,与数组不同的是它的大小可以动态变化。
Vector
的操作很简单,通过 addElement() 加入一个对象,用 elementAt() 取出它,还可以查询当前所保存的对象的个数 size(); 另外还有一个 Enumeration 类提供了连续操作 Vector 中元素的方法,这可以通过 Vector 中的 elements() 方法来获取一个 Enumeration 类的对象,可以用一个 While 循环来遍历其中的元素。用 hasMoreElements() 检查其中是否还有更多的元素。用 nextElement() 获得下一个元素。 Enumeration 的用意在于使你能完全不用理会你要遍历的容器的基础结构,只关注你的遍历方法,这也就使得遍历方法的重用成为可能。由于这种思想的强大功能,所以在 Java2 中被保留下来,不过具体实现,方法名和内部算法都改变了,这就是 Java2 中的 Iterator 以及 ListIterator 类。然而 Enumeration 的功能却十分有限,比如只能朝一个方向进行,只能读取而不能更改等。
另一个单元素容器是 Stack ,它最常用的操作便是压入和弹出,最后压入的元素最先被弹出。你可以想象一个只上面开口的书箱,最后放进去的书一定是最先被拿到,而最先放进去的只有在全部书拿出后才能取出,这种特性被称为后进先出( LIFO )。在 Java Stack 的的用法也很简单,有 push() 压入一个元素,用 pop() 弹出一个元素。然而它的设计却无法让人理解, Stack 继承了 Vector 而不用 Vector 作为其中一个元素类型来实现其功能,这样造成的结果是 Stack 也拥有 Vector 的行为,也就是说你可以把 Stack 当作一个 Vector 来用,而这与 Stack 的用意毫无关系。这应该算为 Java1 1.0/1.1) 中容器类库设计者的一大失误吧,还好,这些在 Java2 中都有了相当大的改变观。
Hashtable
也是 Java1 中一个有用的容器类库。它的基本目标是实现两个或多个对象之间进行关联。举一个现实生活中的例子,比如我们说美国白宫时,指的就是在美国华盛顿的总统办公大楼,为什么一说到美国白宫,总统办公大楼呢?这是我们人为的对 美国白宫 和总统办公大楼进行了关联,本来 美国白宫 就是四个普通的文字,现在却有了不同的含义。在 Java 中我们就可以用 String 定义一个内容为 美国白宫 的对象变量,在定义一个总统大楼的对象变量,把它们进行关联,这就是 Hashtable 的用意。通过使用 pub(Object key,Object value) 方法把两个对象进行关联,需要时用 get(Object key) 取得与 key 关联的值对象。还可以查询某个对象的索引值等等。值得说明的这里的 get 方法查找一个对象时与 Vector 中的 get 方法在内部实现时有很大不同,在一个 Hashtable 中查找一个键对象要比在一个 Vector 中快的多。这是因为 Hashtable 使用了一种哈希表的技术(在数据结构中有详细讲解),在 Java 每个对象缺省都有一个通过 Object hashCode() 方法获得的哈希码, Hashtable 就是利用这个哈希实现快速查找键对象的。
Java1
容器类库设计的另一个重大失误是竟然没有对容器进行排序的工具。比如你想让 Vector 容器中的对象按字典顺序进行排序,你就要自己实现。
虽然 Java1 中的容器类库如此简陋,却也使 Java 程序员在当时编程时省力不少,那些容器类也被大量用到,正所谓无可奈何,没得选择。
可能是 Java 在其成长过程一直被美丽的光环笼照着,所以它的缺点也被人们忽略了,幸好,在 Java2 中容器类库设计者对以前的拙劣设计进行了大刀阔斧的整改,从而使 Java 变得更加完美。
三、Java2中的容器类库
Java1.2 之后 Java 版本统称为 Java2 Java2 中的容器类库才可以说是一种真正意义上的集合框架的实现。基本完全重新设计,但是又对 Java1 中的一些容器类库在新的设计上进行了保留,这主要是为了向下兼容的目的,当用 Java2 开发程序时,应尽量避免使用它们, Java2 的集合框架已经完全可以满足你的需求。有一点需要提醒的是,在 Java1 中容器类库是同步化的,而 Java2 中的容器类库都是非同步化,这可能是对执行效率进行考虑的结果。
Java2
中的集合框架提供了一套设计优良的接口和类,使程序员操作成批的数据或对象元素极为方便。这些接口和类有很多对抽象数据类型操作的 API ,而这是我们常用的且在数据结构中熟知的。例如 Maps Sets Lists Arrays 等。并且 Java 用面向对象的设计对这些数据结构和算法进行了封装,这就极大的减化了程序员编程时的负担。程序员也可以以这个集合框架为基础,定义更高级别的数据抽象,比如栈、队列和线程安全的集合等,从而满足自己的需要。
Java2
的集合框架,抽其核心,主要有三类: List Set Map 。如下图所示:

从图上可以看出, List Set 继承了 Collection ,而 Map 则独成一体。初看上去可能会对 Map 独成一体感到不解,它为什么不也继承 Collection 呢?但是仔细想想,这种设计是合理的。一个 Map 提供了通过 Key Map 中存储的 Value 进行访问,也就是说它操作的都是成对的对象元素,比如 put() get() 方法,而这是一个 Set List 所不就具备的。当然在需要时,你可以由 keySet() 方法或 values() 方法从一个 Map 中得到键的 Set 集或值的 Collection 集。
1
Collection 接口提供了一组操作成批对象的方法,用 UML 表示的方法列表如下:
它提供了基本操作如添加、删除。它也支持查询操作如是否为空 isEmpty() 方法等。为了支持对 Collection 进行独立操作, Java 的集合框架给出了一个 Iterator ,它使得你可以泛型操作一个 Collection ,而不需知道这个 Collection 的具体实现类型是什么。它的功能与 Java1 中的 Enumeration 类似,只是更易掌握和使用,功能也更强大。在建立集合框架时, Sun 的开发团队考虑到需要提供一些灵活的接口,用来操作成批的元素,又为了设计的简便,就把那些对集合进行可选操作的方法与基本方法放到了一起。因为一个接口的实现者必须提供对接口中定义的所有方法的实现,这就需要一种途径让调用者知道它正在调用  的可选方法当前不支持。最后开发团队选择使用一种信号,也即抛出一种不支持操作例外 (UnsupportedOperationException) ,如果你在使用一个 Collection 中遇到一个上述的例外,那就意味着你的操作失败,比如你对一个只读 Collection 添加一个元素时,你就会得到一个不支持操作例外。在你实现一个集合接口时,你可以很容易的在你不想让用户使用的方法中抛出 UnsupportOperationException 来告诉使用者这个方法当前没有实现, UnsupportOperationException RuntimeException 的一个扩展。
另外 Java2 的容器类库还有一种 Fail fast 的机制。比如你正在用一个 Iterator 遍历一个容器中的对象,这时另外一个线程或进程对那个容器进行了修改,那么再用 next() 方法时可能会有灾难性的后果,而这是你不愿看到的,这时就会引发一个 ConcurrentModificationException 例外。这就是 fail fast
2
List 接口对 Collection 进行了简单的扩充,它的具体实现类常用的有 ArrayList LinkedList 。你可以将任何东西放到一个 List 容器中,并在需要时从中取出。 ArrayList 从其命名中可以看出它是一种类似数组的形式进行存储,因此它的随机访问速度极快,而 LinkedList 的内部实现是链表,它适合于在链表中间需要频繁进行插入和删除操作。在具体应用时可以根据需要自由选择。前面说的 Iterator 只能对容器进行向前遍历,而 ListIterator 则继承了 Iterator 的思想,并提供了对 List 进行双向遍历的方法。
3
Set 接口也是 Collection 的一种扩展,而与 List 不同的时,在 Set 中的对象元素不能重复,也就是说你不能把同样的东西两次放入同一个 Set 容器中。它的常用具体实现有 HashSet TreeSet 类。 HashSet 能快速定位一个元素,但是你放到 HashSet 中的对象需要实现 hashCode() 方法,它使用了前面说过的哈希码的算法。而 TreeSet 则将放入其中的元素按序存放,这就要求你放入其中的对象是可排序的,这就用到了集合框架提供的另外两个实用类 Comparable Comparator 。一个类是可排序的,它就应该实现 Comparable 接口。有时多个类具有相同的排序算法,那就不需要在每分别重复定义相同的排序算法,只要实现 Comparator 接口即可。
集合框架中还有两个很实用的公用类: Collections Arrays Collections 提供了对一个 Collection 容器进行诸如排序、复制、查找和填充等一些非常有用的方法, Arrays 则是对一个数组进行类似的操作。
4
Map 是一种把键对象和值对象进行关联的容器,而一个值对象又可以是一个 Map ,依次类推,这样就可形成一个多级映射。对于键对象来说,像 Set 一样,一个 Map 容器中的键对象不允许重复,这是为了保持查找结果的一致性 ; 如果有两个键对象一样,那你想得到那个键对象所对应的值对象时就有问题了,可能你得到的并不是你想的那个值对象,结果会造成混乱,所以键的唯一性很重要,也是符合集合的性质的。当然在使用过程中,某个键所对应的值对象可能会发生变化,这时会按照最后一次修改的值对象与键对应。对于值对象则没有唯一性的要求。你可以将任意多个键都映射到一个值对象上,这不会发生任何问题(不过对你的使用却可能会造成不便,你不知道你得到的到底是那一个键所对应的值对象)。 Map 有两种比较常用的实现: HashMap TreeMap HashMap 也用到了哈希码的算法,以便快速查找一个键, TreeMap 则是对键按序存放,因此它便有一些扩展的方法,比如 firstKey(),lastKey() 等,你还可以从 TreeMap 中指定一个范围以取得其子 Map 。键和值的关联很简单,用 pub(Object key,Object value) 方法即可将一个键与一个值对象相关联。用 get(Object key) 可得到与此 key 对象所对应的值对象。
四、未来的Java容器类库
前面几部分对 Java 中容器类库的过去与现在的状况进行了讨论,然而就在写下此文时, Sun 已经开始通过某种途径分发 J2SE1.5 Alpha 测试版了。在今年的 JavaOne 大会上,诸多大师描绘了 Java 的美好未来与在下一个版本中即将加入的一些新特性,其中为容器类库加入的一个重要特性就是泛型。
其实泛型并不是什么新东西,在其它一些面向对象的语言中早已存在,如 C++ 。泛型的基本目标很简单:能够保证你使用一种类型安全的容器。那么到底怎样一种类型安全呢?我们先看下面这一段没有使用泛型特性的代码:
  1. import java.util.*;
  2. public class Generics{
  3.     /**
  4.      * 输出一个String类型的列表,假设所给参数list中所有元素都为String
  5.      */
  6.     public static void printList(List list){
  7.         for(int i=0;i<list.size();i++){
  8.             System.out.println(((String)list.get(i)).toString());
  9.         }
  10.     }
  11.     public static void main(String[] args){
  12.         List list=new ArrayList();
  13.         for(int i=0;i<9;i++){
  14.             list.add("Number:"+Integer.toString(i));
  15.         }
  16.         //list.add(new Generics());  //(1)
  17.         printList(list);
  18.     }
  19. }
上面的代码很简单,定义了一个静态方法来打印一个元素为 String 类型的 List ,然而正如你看到的一样,如果你试着将 (1) 行中前面的注释去掉,你就会得到一个 ClassCastException 例外,因为 printList 会将 list 中的每个元素都转型为 String ,而在遇到最后一个元素时发现它是一个 Generics 类型从而无法完成转型,例外就被抛出。这种情况在 Java 编程中很容易出现,原因是 Java 的容器类库通常保存的是 Object 类型,而这是所有类的直接或间接超类,这就允许你将任何类型的元素添加到一个 List 中而不会给你任何提示,有经验的程序员可能会自己编写一个容器类来限制添加到其中的元素,这是一个编程技巧。但是现在我们就再也不用那样做了,泛型机制会为我们做好这件事。那就看一下用泛型机制对上面代码进行的改进:
  1. import java.util.*;
  2. public class Generics{
  3.     /**
  4.      * 输出一个String类型的列表,限制了所给参数list中所有元素都为String
  5.      */
  6.     public static void printList(ArrayList<String> list){
  7.         for(int i=0;i<list.size();i++){
  8.             System.out.println(list.get(i).toString());
  9.             //get()返回的不再是Object类型,而是String类型
  10.         }
  11.     }
  12.     public static void main(String[] args){
  13.         ArrayList list=new ArrayList<String>(); //注意此行中声明语法的变化
  14.         for(int i=0;i<9;i++){
  15.             list.add("Number:"+Integer.toString(i)); //只能向其中添加String类型
  16.         }
  17.         list.add(new Generics());  //无法通过,编译时错误
  18.         printList(list);
  19.     }
  20. }

正如在代码中所看到的,容器的声明有了变化,即在一个容器类后面用 <> 来说明你想要放入这个容器中的元素类型,那么接下来你只能向这个容器加那种类型,否则编译就无法通过。在 printList 中也省去了转型的麻烦。当然有了泛型,并不是说以前的声明方法不能用了,你完全可以还用以前的方法,这没有任何问题。其实根据 JSR 中对 for 语句功能的增强,遍历一个容器也会更加简单。
当然泛型的使用方法不只如此,这里并没有对它进行完整描述,只想告诉你,泛型确实为我们编程提供了便利,但是仍然需要用心去学习和掌握。
随着 Java 的进一步完善,它的功能和易用性也得到提高,我有理由相信 Java 在计算机语言中所占的位置也会更加牢固,让喜爱 Java 的人更加喜爱它。祝愿 Java 一路走好!
 
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/一键难忘520/article/detail/802860
推荐阅读
相关标签
  

闽ICP备14008679号