JAVA集合框架综述

来源:互联网 发布:软件fmea可靠性分析 编辑:程序博客网 时间:2024/05/18 13:06

一、提出背景
在编写 Java 程序中,我们最常用的除了八种基本数据类型,String 对象外还有一个集合类,在我们的程序中到处充斥着集合类的身影!Java 中集合大家族的成员实在是太丰富了,有常用的 ArrayList、HashMap、HashSet,也有不常用的 Stack、Queue,有线程安全的 Vector、HashTable,也有线程不安全的 LinkedList、TreeMap 等等!

JAVA实用类库提供了一套相当完整的容器类来解决一些问题(写程序时并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,数组尺寸固定这一限制显得太过受限),容器类中最基本的类型是List、Set、Queue和Map,由于JAVA类库中用Collection集合类来指代该类库的一个特殊子集,所以用容器类来称呼它们。这里我们要讨论的是Collection集合类。

二、集合框架(Collections Framework)
Java提供了数据持有对象的方式,以及对象集合的操作。集合在Java中是非常重要的,Java集合框架API(Application Programming interface应用程序接口)是用来表示和操作集合的统一框架,它包含接口、实现类以及一些编程辅助算法,具体位于java.util包下。

集合代表了一组对象(和数组一样,但数组长度不能变,而集合能)。Java中的集合框架定义了一套规范,用来表示、操作集合,而操作不外乎“增删改查”四种。

1、JAVA集合框架UML类图
集合框架UML类图
根据上面的UML例图,可以看出的是Java集合框架大体上可以简要分为四部分: Collection接口及其实现类、Map接口及其实现类、Iterator接口及实现类、辅助工具类。

2、两大基类Collection与Map
在集合框架的类继承体系中,最顶层有两个接口:

·Collection表示一组纯数据·Map表示一组key-value

一般继承自Collection或Map的集合类,会提供两个“标准”的构造函数:

·没有参数的构造函数,创建一个空的集合类·有一个类型与基类(Collection或Map)相同的构造函数,创建一个与给定参数具有相同元素的新集合类

因为接口中不能包含构造函数,所以上面这两个构造函数的约定并不是强制性的,但是在目前的集合框架中,所有继承自Collection或Map的子类都遵循这一约定。

下面讲述Java集合框架的四部分Collection接口及其实现类、Map接口及其实现类、Iterator接口及实现类、辅助工具类。
3.1、Collection接口及其实现类
Collection接口UML类图
3.1.1、Connection接口
Collection接口是最基本的集合接口,一个 Collection 代表一组 Object,继承至Iterable接口(主要通过其产生迭代器逐一地进行元素访问)。其中的元素允许重复,可以无序。Java不提供直接继承自Collection的类,只提供继承于它的子接口(如List和set)。
Collection 所代表的是一种规则,它所包含的元素都必须遵循一条或者多条规则。如有些允许重复而有些则不能重复、有些必须要按照顺序插入而有些则是散列,有些支持排序但是有些则不支持。
如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

Iterator it = collection.iterator(); // 获得一个迭代子while(it.hasNext()) {    Object obj = it.next(); // 得到下一个元素}

对于其他具体的方法,可直接查阅源码。

3.1.2、List接口
作为Collection的子接口,List限定容器中元素是有序的,也就是说使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素在List中位置,类似于数组的下标)来访问List中的元素,而且允许有相同的元素或null元素。
除了具有Collection接口必须的iterator方法之外,List还提供了一个listiterator()方法,和标准的相较而言多了一些add()之类的方法,允许添加,删除、设定元素,重要的是能够实现向前向后遍历。其常用的实现类有ArrayList,LinkedList,vector和stack(vector<-stack)。

①ArrayList
ArrayList是一个动态数组,该类也是实现了List的接口,实现了可变大小的数组,随机访问和遍历元素时,提供更好的性能。它允许任何符合规则的元素插入甚至包括 null。每一个 ArrayList 都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。

该类也是非同步的,在多线程的情况下不要使用。ArrayList 增长当前长度的50%,插入删除效率低。

②LinkedList
LinkedList同样实现 List 接口,而 LinkedList 与 ArrayList 不同,ArrayList是一个动态数组,而 LinkedList 是一个双向链表,允许有null(空)元素。所以它除了有 ArrayList 的基本操作方法外还额外提供了 get,remove,insert 方法在 LinkedList 的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。

由于实现的方式不同,LinkedList 不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在 List 中进行插入和删除操作。

与 ArrayList 一样,该类没有同步方法,如果多个线程同时访问一个List,则必须自己实现访问同步,解决方法就是在创建List时候构造一个同步的List。例如:

Listlist=Collections.synchronizedList(newLinkedList(...));

另外LinkedList 查找效率低。

③Vector
与 ArrayList 相似,但是 Vector 是同步的。所以说 Vector 是线程安全的动态数组。它的操作与 ArrayList 几乎一样。可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。由Vector创建的Iterator,虽然和 ArrayList创建的Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了 Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出 ConcurrentModificationException,因此必须捕获该异常。

④Stack
栈,Stack 继承自 Vector,实现一个后进先出的堆栈,Stack 刚创建后是空栈。Stack 提供 5 个额外的方法使得 Vector 得以被当作堆栈使用。基本的 push 和 pop 方法,还有 peek 方法得到栈顶的元素,empty 方法测试堆栈是否为空,search 方法检测一个元素在堆栈中的位置。

3.1.3、Set接口
Set是一种不包括重复元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,由于 Set 接口的特殊性,所有传入 Set 集合中的元素都必须不同,同时要注意任何可变对象(Mutable Object),如果在对集合中元素进行操作时,一个Set中的可变元素改变了自身状态导致e1.equals(e2)==true,则必定会产生某些问题。它维持它自己的内部排序,所以随机访问没有任何意义。与 List 一样,它同样允许 null 的存在但是最多只能有一个null。实现了 Set 接口的集合有:EnumSet、HashSet、TreeSet。

List和Set的异同
同:①List和Set都是继承自Collection接口
异:②List特点:元素有放入顺序,元素可重复,适合追加/删除数据,随机取数效率较低。
Set特点:元素无放入顺序,元素不可重复(虽无放入顺序,但元素在Set中位置由该元素的HashCode决定,其位置其实是固定的),适合随机储存/插入/删除数据,遍历效率较低
③List接口有实现类:ArrayList、LinkedList、Vector、Stack
Set接口有实现类: HashSet(底层由HashMap实现)、TreeSet、LinkedHashSet

①EnumSet
是枚举的专用 Set。所有的元素都是枚举类型。

②HashSet
HashSet 堪称查询速度最快的集合,因为其内部是以HashCode来实现的。它内部元素的顺序是由哈希码来决定的,所以它不保证 set 的迭代顺序;特别是它不保证该顺序恒久不变。

③TreeSet
基于 TreeMap,生成一个总是处于排序状态的 set,内部以 TreeMap 来实现。它是使用元素的自然顺序对元素进行排序,或者根据创建 Set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。

能够想到的是Set的构造函数一定有一个约束条件,限制传入的Collection不能包含重复的元素。
到底是如何进行限定的呢?我们先看其HashSet的构造器吧。
这里写图片描述
理所当然的我们应该进入addAll()方法。进入其方法内部,由于HashSet是继承AbstractCollection(提供Collection的骨干实现)。addAll方法由AbstractCollection实现。
这里写图片描述
阅读源码可以知道,要想找到关键所在,就要进入add方法了,不过这里我们查看在AbstractCollection类中查看add方法是他是空的。貌似断了……想想其实不然,时刻需要留意的是java中的多态性,我们看看子类中的实现吧:
这里写图片描述
我们又得进入HashMap类中去查看put方法的实现。(HashSet基于HashMap实现)

private void inflateTable(int toSize) {    //辅助函数,用于填充HashMap到指定的capacity    // Find a power of 2 >= toSize    int capacity = roundUpToPowerOf2(toSize);    //threshold为resize的阈值,超过后HashMap会进行resize,内容的entry会进行rehash    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);    table = new Entry[capacity];    initHashSeedAsNeeded(capacity);}/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. */public V put(K key, V value) {    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }    if (key == null)        return putForNullKey(value);    int hash = hash(key);    int i = indexFor(hash, table.length);    //这里的循环是关键    //当新增的key所对应的索引i,对应table[i]中已经有值时,进入循环体    for (Entry<K,V> e = table[i]; e != null; e = e.next) {        Object k;        //判断是否存在本次插入的key,如果存在用本次的value替换之前oldValue,相当于update操作        //并返回之前的oldValue        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    //如果本次新增key之前不存在于HashMap中,modCount加1,说明结构改变了    modCount++;    addEntry(hash, key, value, i);    return null;}void addEntry(int hash, K key, V value, int bucketIndex) {    //如果增加一个元素会后,HashMap的大小超过阈值,需要resize    if ((size >= threshold) && (null != table[bucketIndex])) {        //增加的幅度是之前的1倍        resize(2 * table.length);        hash = (null != key) ? hash(key) : 0;        bucketIndex = indexFor(hash, table.length);    }    createEntry(hash, key, value, bucketIndex);}

3.1.4、Queue接口
队列,它主要分为两大类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括 ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。

3.2、Map接口及其实现类
Map接口的UML类图
Map 映射表(关联数组)与 List、Set 接口不同,它是由一系列键值对(关联)组成的集合,提供了 key 到 Value 的映射。同时它也没有继承Collection。Tips:误区:不要将Collection认为是Java集合中的顶级接口,Map和Collection在层次结构上没有必然的关系。
在 Map 中它保证了 key 与 value 之间的一一对应关系。也就是说一个 key 对应一个 value,所以它不能存在相同的 key 值,当然 value 值可以相同。实现 map 的有:AbstractMap、HashMap、TreeMap、HashTable、Properties、EnumMap。

①AbstractMap
提供Map接口的骨干实现

②HashMap
HashMap是非同步的,以哈希表数据结构实现,且允许null元素,即null value和null key。查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个 hash 表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可以通过查看 HashMap.Entry 的源码发现它是一个单链表结构。但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销与HashMap 的容量成比例。因此,如果看重迭代操作的性能的话,不要将HashMap的初始化容量设得过高,或者load factor过低。

③HashTable
实现一个key-value映射的哈希表,但与HashMap的区别在于,HashMap是非同步的,而Hashtable是同步的,且HashTable中任何非空(non-null)的对象都可作为key或者value。解决冲突时与 HashMap 也一样也是采用了散列链表的形式,不过性能比HashMap要低,

添加数据使用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方法,而不要只写其中一个。
  

④SortedMap接口
进一步提供关于键的总体排序的Map。该映射是根据其键的自然顺序进行排序的,或者根据通常在创建有序映射时提供的 Comparator 进行排序。
⑤TreeMap
键以某种排序规则排序,该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator 进行排序,具体取决于使用的构造方法。TreepMap是线程非同步的。内部以 red-black(红-黑)树数据结构实现,实现了 SortedMap 接口。

3.3、Iterator接口及实现类
迭代器设计模式,通过它我们可以遍历并选择集合中的对象,对于其集合的底层结构实现并不关心。对于其详细内容和具体用法,后续整理。

3.4、辅助工具类
这笔者想当然的分类的吧,不过也不打紧,便于思考理解就行。

Collections、Arrays类
Collections、Arrays是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

Comparable、Comparator接口
一般是用于对象的比较来实现排序,两者略有区别。
Comparable用作默认的比较方式,实现了该接口的类之间可以相互进行比较,这个对象组成的集合就可以直接通过sort()进行排序了。
Comparator是设计模式中策略模式的一种应用。将算法的实现和数据进行了分离。
一般用在如下情况下:
1、类设计者没有考虑到比较问题而没有实现Comparable接口。这是我们就可以通过使用Comparator,这种情况下,我们是不需要改变对象的。
2、一个集合中,我们可能需要有多重的排序标准,这时候如果使用Comparable就有些捉襟见肘了可以自己继承Comparator提供多种标准的比较器进行排序。
至于两者的具体使用语法,读者可查阅API或者Google!

4、相互区别与比较
上面介绍了这么多集合类,可能有些让人懵逼的感觉,不知道什么时候用什么集合类,下面就介绍一下比较和用途。

4.1、Vector和ArrayList

1、同步性
vector是线程同步的,所以它也是线程安全的,这个类中的一些方法保证了Vector中的对象是线程安全的。而arraylist是线程异步的,因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销,效率比较高。

2、数据增长
从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度时,它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度(增长率为目前数组长度的100%),ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。

3、使用模式
在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素,vector和arraylist使用的时间是相同的,都是0(1),这个时候使用vector和arraylist都可以。
但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长,如果移动一个指定位置的数据花费的时间为0(n-i),n为总长度,这个时候就应该考虑到使用linklist,因为它移动一个指定位置的数据所花费的时间为0(1),而查询一个指定位置的数据时花费的时间为0(i)。
ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以索引数据快,插入数据慢。Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差,LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快!

4.2、ArrayList和LinkedList
1、ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。

2、对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。

3、对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
这一点要看实际情况。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入或删除数据,LinkedList的速度大大优于ArrayList。因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

4.3、HashMap与TreeMap
“集合框架”提供两种常规的Map实现:HashMap和TreeMap(TreeMap实现SortedMap接口)
1、HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。

2、在Map 中插入、删除和定位元素,HashMap是最好的选择。但如果要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确地定义了hashCode()和equals()的实现。TreeMap没有调优选项,因为该树总处于平衡状态。

3、两树map一样,但顺序不一样,导致hashCode()不一样。
同样做测试:
在hashMap中,同样的值的map,顺序不同,equals时,false。
而在treeMap中,同样的值的map,顺序不同,equals时,true,说明,treeMap在equals()时是整理了顺序的。

4.4、HashTable与HashMap
1、历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现。

2、同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的。

3、值:只有HashMap可以让你将空值作为一个表的条目的key或value。

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

参考文献:
http://liujiacai.net/blog/2015/09/01/java-collection-overview/
http://wiki.jikexueyuan.com/project/java-enhancement/java-twenty.html
http://blog.csdn.net/softwave/article/details/4166598
http://blog.csdn.net/lcore/article/details/8868943
http://www.runoob.com/java/java-collections.html