集合类框架

集合类框架说明

  java集合工具类框架图如下:   

说明:

  1. Collection接口是java集合类的顶层接口,Map不属于Collection接口下的体系。Collection接口继承了Iterable接口,说明所有Collection实例都支持使用迭代器进行访问。Collection作为顶层接口主要声明了子类共有的一些方法,如size(),isEmpty(),contains(),add(),remove(),clear()等等方法。

  2. AbstractCollection是实现了Collection接口的抽象类,主要作用是要复用集合类一些公用的方法。

  3. 从Collection接口派生出两个子接口List和Set,List代表了有序(次序而非大小顺序)可存在重复元素的列表对象,而Set代表了不要求有序并且不能存在重复元素的集合对象。可以将Collection体系一分为二,分别从List体系和Set体系来记忆和理解。

  4. 对于List体系,最顶层的接口是List接口,继承自Collection接口,同样有一个抽象类AbstactList用来复用List的公共属性和方法,而按照线性存储方式还是链式存储方式,又可以将List的实现类分为两组——线性存储:ArrayList、Vector、Stack;链式存储:LinkedList。    ArrayList是动态数组,Vector是ArrayList的线程安全版本,新增了使用枚举对象Enumeration遍历的方法,Stack是Vector的子类,主要是用作顺序栈;LinkedList是链式存储的双向队列结构,它不是直接复用AbstractList而是中间加多了一个AbstractSequentialList。同时LinkedList实现了Deque(双向队列)接口。

  5. 对于Set体系就相对简单一些,日常开发用到的一般是HashSet、LinkedHashSet、TreeSet。对于Set我们需要知道其底层是根据对应的Map进行实现,然后重点学习List和Map。另外不是对于任何Set都是无序的,虽然前面说过Set是不要求有序的,但是具体到某一个子类,它可以选择有序的实现也可以不实现有序,这一点接口并无强制规定一定是无序的,像LinkedHashSet和TreeSet就是有序的Set。

Collection与Collections

Collection接口是java集合类的顶层接口,而Collections是针对集合类的一个帮助类,提供了一系列的静态方法实现对各种集合的搜索、排序、线程安全化等操作。

List和Set接口是继承自Collection接口的子接口,对于集合类中的boolean add(Object o)方法,返回的布尔值不表示添加的成功与否,而是表示执行add()操作后,集合的内容(元素的数量、位置)是否被改变了,其余addAll、remove、removeAll、remainAll方法的返回值也是如此。

Collection接口继承了Iterable接口,遍历集合可以使用迭代器,典型的用法如下:

Iterator it = collection.iterator();
while(it.hasNext()) {
    Object o = it.next();
}

List接口

List是有序的Collection,有序是指元素的次序而非大小顺序,同时也是允许有相同元素的Collection。实现List接口的常用类有ArrayList、LinkedList、Vector、Stack。

ArrayList、Vector、Stack是顺序存储结构,LinkedList是链式存储结构,都允许插入null元素

ArrayList

扩容: ArrayList是动态数组,可以根据插入的元素的数量自动扩充容量,而使用者不需要关心它是什么时候扩容的,只要把它当成足够大的数组来使用就行了。ArrayList在插入元素的时候都会检查当前的数组大小是否足够,如果不够,就会进行扩容,然后将原先数组中的元素全部复制到新数组中,这个操作比较耗时,所以在创建ArrayList对象时如果元素的数量Capactity能够预估,可以指定ArrayList的初始容量。

访问与插入删除操作: ArrayList可以直接根据下标索引访问元素,因此get()方法是O(1),而插入和删除时可能需要发生元素的移动复制,因此add()和remove()方法时间复杂度是O(n)。

非线程安全: ArrayList没有同步,属于非线程安全,在多线程环境下如果出现多个线程需要同时访问ArrayList对象并且存在修改其中元素的操作时,可以使用Collections.synchronizedList(new ArrayList()); 方法返回一个同步的实例或者使用JUC下的集合类代替。

LinkedList

访问与插入删除操作: LinkedList 内部实现是使用双向链表的方式来保存元素,因此插入与删除元素的性能较ArrayList好,但随机访问元素就需要遍历链表才能找到,性能上比ArrayList差,不过LinkedList底层做了优化,get()操作不需要去遍历整个链表,而是通过一个指针记录了中间结点的位置,当需要根据下标获取一个元素时,首先是判断目标元素是在左半部分还是右半部分,然后只需要查找链表的一半即可。因此在需要频繁随机访问元素的情况下建议使用ArrayList,在需要频繁随机插入和删除的情况下建议使用LinkedList。

可用来实现栈、队列、双向队列:LinkedList实现了Dequ接口,增加了addFirst()、addLast()、removeFirst()、removeLast()……等方法,可以将LinkedList当成栈、队列、双向队列来使用,但此时不能使用多态,因为List接口没有定义这些方法.

Vector

Vector是ArrayList的线程安全版本,通过同步方法进行同步,支持多线程访问,默认初始容量与ArrayList一样是10,扩容算法上也不同,Vector是扩大为原来的两倍,也可以根据构造函数传入的增长长度扩大。

Stack

Stack继承于Vector,是一个线程安全的顺序栈。

Set接口

Set是无序的Collection,同时也是不允许有相同元素的Collection(equals等于true)。接口相当于一个契约,规定了实现该接口的类需要遵循的行为,Set要求其中的元素是不重复的,但是对存储顺序不作保证和限制,有序或者无序的实现类都可以,SortedSet接口就规定了其中的元素必须是有序的,其实现类TreeSet就是有序的。

HashSet的底层是基于HashMap实现的(LinkedHashSet基于LinkedHashMap,TreeSet基于TreeMap),Set中的元素其实是放在了底层HashMap的Key上,而value是一个Object对象标志,相关的HashSet的操作也是直接调用HashMap来完成的。

Map接口

Map集合类没有继承Collection接口,它是一种“链表数组”的结构,提供key到value的映射。一个Map中key是唯一的,每个key只能映射一个value。可以把Map看成一组Key-Value集合。

HashMap

HashMap是非线程安全的,允许Key和Value都为空,其Key使用的哈希值是内部另外实现的方法计算而来,默认初始容量是16,当容量达到负载因子(load factor)大小时重新分配空间进行再散列扩容,而且一定是以2的指数去扩容。

当程序试图将一个key-value对放入 HashMap 中时,程序首先根据该key的hashCode()返回值决定该Entry的存储位置:如果两个Entry的key的hashCode()返回值相同,那它们在数组中的下标相同。如果这两个Entry的key通过equals比较返回true,新添加Entry的value将覆盖集合中原有Entry的 value,但key不会覆盖。如果这两个Entry的key通过equals比较返回false,新添加的Entry将与集合中原有Entry形成Entry 链,而且新添加的 Entry 位于 Entry 链的头部,即使用头插法插入,如果使用尾插法需要额外记录链尾。

迭代HashMap的时间复杂度跟其容量是成正比关系,如果把HashMap的容量设置得过高,或者负载因子(load factor)过低会使迭代性能下降 。遍历Map一般使用map.entrySet().iterator()拿到迭代器进行遍历。

LinkedHashMap

LinkedHashMap是HashMap的子类,底层是一个哈希表+双重链表的结构,它默认迭代时使用用户插入的顺序输出,(通过增加一个head头结点的双重链表记录插入顺序) LinkedHashMap在执行get和put方法的时候会调用recordAccess方法,如果设置了LinkedHashMap访问的时候按照访问的顺序迭代,也就是将accessOrder属性设置为true,那么在recordAccess方法中将会从原来的链表中删除掉已存在的entry,然后将新entry使用头插法插入链表,这样就能保证链表尾部的entry是最久未使用的。

依靠这个特点,可以使用LinkedHashMap实现LRU算法。每次淘汰最久未用元素的时候刚好是删除链表的最后一个结点,LinkedHashMap维持的是双向链表,可以直接通过头结点访问最后一个元素因此很容易就能删除最后一个结点。那么什么时候删除呢?也就是如果我们想要用LinkedHashMap实现LRU算法该怎么做呢?从源码可以看到,在调用put方法添加元素,执行到addEntry()方法时,会判断removeEldestEntry()方法是否访问true,即是否超过容量删除“最老”元素,默认该方法是返回false,不删除,即让LinkedHashMap的元素永远不过期,实现LRU算法就是要重写removeEldestEntry方法。

Hashtable

Hashtable是HashMap的线程安全版本,其不同之处在于其默认大小是11,不允许空的Key和Value,存储时使用的hash值是直接使用对象的hashCode,再散列扩容时增长的方式是原本的长度*2+1。

TreeMap

TreeMap是jdk提供的唯一有序Map实现类,是利用红黑树来实现的,所以其最坏的插入、删除、查找的时间复杂度是O(logn);

TreeMap默认情况下是根据key的自然顺序进行排序,也可以在构造函数中指定自己的比较器,同时它也是线程非安全的,支持fail-fast机制。

TreeMap的键值不能为空,value值可以为空

WeakHashMap

WeakHashMap与HashMap最大的不同是WeakHashMap的键是弱键(WeakReference),当某个键不再被使用时,会从WeakHashMap中自动移除。

WeakHashMap是通过 WeakReference 和 ReferenceQueue实现的:

  1. 新建WeakHashMap,将key-value添加进WeakHashMap中,即保存进WeakHashMap的table数组中
  2. 当某个键不再被其他对象引用后,在GC进行回收的时候会将该弱键回收,并同时将该弱键加入到ReferenceQueue中,即queue属性中
  3. 当我们再次操作WeakHashMap时,会先同步table和queue,table保存了全部的键值对,而queue中保存的是被GC回收的键值对,即删除table中被GC回收的键值对。

Fail-Fast机制

Fail-Fast机制(快速失败机制)是一种集合类的错误检测机制,直白的说就是当出现遍历和修改操作同时进行时能快速的报错,比如当多个线程对同一个集合进行操作时,如果有线程对该集合进行添加或删除元素的操作 ,那么其他线程在访问该集合时就会抛出ConcurrentModificationException异常,产生Fail-Fast事件; 或者当使用Iterator遍历集合时对该集合进行添加或删除也会发生Fail-Fast事件。

Fail-Fast与是否线程安全没有直接关系。产生Fail-Fast的机制是:AbstractList中定义了一个modCount和expectedModCount属性,都表示该集合对象元素个数修改的次数,只要涉及到修改集合中的元素个数时,都会改变他们的值,在使用迭代器遍历的时候需要判断他们是否相等,如果不相等就会抛出ConcurrentModificationException,而在获取迭代子之后迭代子中的expectedModCount就不会再发生改变了。

JUC下的类不会产生Fail-Fast是因为像CopyOnWriteArrayList,在进行改变内容的操作(add、remove、clear)时,都会copy一份现有数据,然后在快照版本上进行修改操作,最后再把原有数据引用指向修改后的数据。

猜你喜欢

转载自my.oschina.net/u/3761681/blog/1601329
今日推荐