集合框架

Java集合框架(Java Collections Framework,JCF),类似与C++中的标准模板库(Standard Template Library,STL,
大学时候想学结果倒也没怎么学)主要是对一些数据结构和相关算法的封装。

JCF的全家幅,如下图说示:

上图实在有点复杂,对初学者来说有点难度,我们先看个简单点的。
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap
集合主要分为两个接口,Collection和Map接口。
Collection是最基本的集合接口,一个Collection代表一组对象,即Collection的元素(Elements)。
Collection提供关于集合的一些通用操作的接口,包括插入(add()方法)、删除(remove()方法)、
判断一个元素是不是其成员(contains()方法)、遍历(iterator()方法)等等
一些Collection允许相同的元素而另一些不行。一些能排序而另一些不行。如:
Set接口体现的是“无序集”的概念,它是不允许有重复元素出现的;List接口代表“有序集”,允许有重复元素;而Map接口则是“映射”关系
Java SDK不提供直接继承自Collection的类,它提供的类都是继承自Collection的“子接口”如List和Set。
有了“无序集”,“有序集”和“映射”,我们就可以定义各种各样的抽象数据结构了,如向量,链表,堆栈,哈希表,平衡二叉树等。
下面让我们来了解下各个子接口下实实在在的类。
     如何遍历集合中的元素
     使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:
    Iterator it = collection.iterator(); // 获得一个迭代子
    while(it.hasNext()) {
      Object obj = it.next(); // 得到下一个元素
    }
下面来介绍下关于List和Set的使用。
   List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。
   用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
   和下面要提到的Set不同,List允许有相同的元素。
  除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个ListIterator接口,
   和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。
  实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。
  LinkedList类
  LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。
    这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
  注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。
    一种解决方法是在创建List时构造一个同步的List:
    List list = Collections.synchronizedList(new LinkedList(...));

ArrayList类
  ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。
    size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。
    其他的方法运行时间为线性。每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。
    这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。
    当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
  和LinkedList一样,ArrayList也是非同步的(unsynchronized)。
Vector类
  Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的Iterator是同一接口,
    但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),
    这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。

Stack 类
  Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。
    基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。
    Stack刚创建后是空栈。
Set接口
  Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。
  很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。
Hashtable类
  Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。
  添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。
Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。
使用Hashtable的简单示例如下,将1,2,3放到Hashtable中,他们的key分别是”one”,”two”,”three”:
    Hashtable numbers = new Hashtable();
    numbers.put(“one”, new Integer(1));
    numbers.put(“two”, new Integer(2));
    numbers.put(“three”, new Integer(3));
  要取出一个数,比如2,用相应的key:
    Integer n = (Integer)numbers.get(“two”);
    System.out.println(“two = ” + n);
  由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。
    hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,
    即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,
    如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,(这个学过数据结构的人都应该知道吧)
    所以尽量定义好的hashCode()方法,能加快哈希表的操作。
  如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,
    只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。
  Hashtable是同步的。
HashMap类
  HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。
    但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。
    因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。
   
他们之间又是怎么区分的呢?什么时候又要用到谁呢?

1. 侧重点:遍历 vs. 修改
遍历和修改似乎是一对矛盾,一个可以高效率插入删除元素的数据结构通常遍历的性能并不是最优。
于是JCF在这里根据用户的目标实现了两种定制的数据结构:哈希表(包括HashSet和HashMap)和平衡二叉树(包括TreeSet和TreeMap)。
由于可排序性是一种独特的要求,所以引入了SortedSet和SortedMap,它们分别是AbstractSet和AbstractMap的子接口,
而TreeSet和TreeMap又分别是他们的一种实现。熟悉数据结构的人可能比较了解,哈希表在进行插入、删除、查找这样的操作是很快的,
其时间复杂度是常数级O(1);平衡二叉树虽然插入、删除操作比较麻烦(需要O(log n)的代价),但进行遍历和排序却很快。
选择完全在于用户的侧重点,但由于类型转换的方便性,通常我们用哈希表构造一个集合以后,再把它转换成相应的树集进行遍历,
以获得较好的效果。
  
2. 历史实现 vs. 新实现
    历史实现(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的存取访问效率更高。关于同步我们下面还将要谈到。
    (我刚开始的时候,我们的项目里用的都是Vector ,其实是错误的,或则说性能不高的。因为那些项目涉及的都
    是单线程,没有线程安全的概念,所以都应该使用ArrayList)这两者的比较我们在以后还会出现。这里只稍微提及下。

Hashtable vs. HashMap
    Hashtable是Dictionary的子类,属于历史实现,而HashMap是Map的子类,是新实现。
    它们的区别除了上面所说的key和value是否可以为空之外,也有同步的差别,Hashtable是同步的,
    但HashMap不是。不过不要因为Hashtable是“老前辈”而瞧不起它哦,它的一个著名的子类Properties我们可是经常会用到的。
   
3. 同步 vs. 不同步
    从上面的描述中我们似乎可以得出这么一个印象:历史实现好像都是同步的,但新实现中却没有。需要同步操作的理由是,
    可能存在多个线程对同一个集合进行操作的情况:譬如一个线程正在对某集合进行遍历,但与此同时,
    另一个线程又在对该集合进行插入或删除,那么第一个线程的遍历结果将是不可预测的,对于同步集合,
    它将会抛出一个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:交换一个线性表中两个元素的位置。
 ……
 5. 泛型
    目前我们了解的JCF的一个重要特征是:所有加入到集合当中的对象都将在表面上失去它们自己的特性,
    而看上去仅仅只是一个Object对象而已,除非你把它强制类型转换成它们原来的对象。这一点很自然,集合嘛,对象的容器,
    它容纳的是各种各样的对象,而不仅仅是某种特定类型的对象。J2SE 5.0出现以后,JCF开始引入泛型的特性,
    譬如我们经常碰到这样的应用,就是把集合转换成特定的数组,虽然Collection有toArray()的方法,但可惜的是,
    这个数组的所有元素都是Object类型的,我们通常的做法是用一个for循环把数组的每个元素都进行强制类型转换,
    虽然可行,但看上去很笨拙,如果有了泛型,我们就可以预先指定要得到的类型,然后一次toArray就可以得到我们期望的数组,
    里面的元素全部都是指定类型了。惭愧的是,我对5.0还不是太了解,具体可以参考J2SE 5.0的JCF文档
 6,使用Vector还是ArrayList?
   (1) Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,
   因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,
   所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。
   (2)数据增长
   从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,
   如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,
   ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。
   所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。
   (3)使用模式
   在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,
    这个时间我们用O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),
    其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?
    以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行位移的操作。这一切意味着什么呢?
    这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。
    如果是其他操作,你最好选择其他的集合操作类。比如,LinkList集合类在增加或移除集合中任何位置的
    元素所花费的时间都是一样的?O(1),但它在索引一个元素的使用缺比较慢-O(i),其中i是索引的位置.
    使用ArrayList也很容易,因为你可以简单的使用索引来代替创建iterator对象的操作。LinkList也会为每个插入的元素创建对象,
    所有你要明白它也会带来额外的开销。
  最后,在《Practical Java》一书中Peter Haggar建议使用一个简单的数组(Array)来代替Vector或ArrayList。
  尤其是对于执行效率要求高的程序更应如此。因为使用数组(Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作。

总结
  如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,
    应该使用ArrayList。如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,
    如果多个线程可能同时操作一个类,应该使用同步的类。要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
  尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,
    客户端代码不用改变。这就是针对抽象编程。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值