JDK提供了大量优秀的集合实现供开发者使用,合格的程序员必需要可以经过功能场景和性能需求选用最合适的集合,这就要求开发者必须熟悉Java的经常使用集合类。本文将就Java Collections Framework中经常使用的集合及其特色、适用场景、实现原理进行介绍,供学习者参考。固然,要真正深刻理解Java的集合实现,仍是要推荐去阅读JDK的源码。node
Java提供的众多集合类由两大接口衍生而来:Collection接口和Map接口程序员
Collection接口算法
Collection接口定义了一个包含一批对象的集合。接口的主要方法包括:数组
size() - 集合内的对象数量 add(E)/addAll(Collection) - 向集合内添加单个/批量对象 remove(Object)/removeAll(Collection) - 从集合内删除单个/批量对象 contains(Object)/containsAll(Collection) - 判断集合中是否存在某个/某些对象 toArray() - 返回包含集合内全部对象的数组 等安全
Map接口多线程
Map接口在Collection的基础上,为其中的每一个对象指定了一个key,并使用Entry保存每一个key-value对,以实现经过key快速定位到对象(value)。Map接口的主要方法包括:架构
size() - 集合内的对象数量 put(K,V)/putAll(Map) - 向Map内添加单个/批量对象 get(K) - 返回Key对应的对象 remove(K) - 删除Key对应的对象 keySet() - 返回包含Map中全部key的Set values() - 返回包含Map中全部value的Collection entrySet() - 返回包含Map中全部key-value对的EntrySet containsKey(K)/containsValue(V) - 判断Map中是否存在指定key/value 等并发
在了解了Collection和Map两大接口以后,咱们再来看一下这两个接口衍生出来的经常使用集合类:性能
List类集合学习
图片.png
List接口继承自Collection,用于定义以列表形式存储的集合,List接口为集合中的每一个对象分配了一个索引(index),标记该对象在List中的位置,并能够经过index定位到指定位置的对象。
List在Collection基础上增长的主要方法包括:
get(int) - 返回指定index位置上的对象 add(E)/add(int, E) - 在List末尾/指定index位置上插入一个对象 set(int, E) - 替换置于List指定index位置上的对象 indexOf(Object) - 返回指定对象在List中的index位置 subList(int,int) - 返回指定起始index到终止index的子List对象 等
List接口的经常使用实现类:
ArrayList
ArrayList基于数组来实现集合的功能,其内部维护了一个可变长的对象数组,集合内全部对象存储于这个数组中,并实现该数组长度的动态伸缩
ArrayList使用数组拷贝来实现指定位置的插入和删除:
插入:
图片.png
删除:
图片.png
LinkedList
LinkedList基于链表来实现集合的功能,其实现了静态类Node,集合中的每一个对象都由一个Node保存,每一个Node都拥有到本身的前一个和后一个Node的引用
LinkedList追加元素的过程示例:
图片.png
ArrayList vs LinkedList
ArrayList的随机访问更高,基于数组实现的ArrayList可直接定位到目标对象,而LinkedList须要从头Node或尾Node开始向后/向前遍历若干次才能定位到目标对象 LinkedList在头/尾节点执行插入/删除操做的效率比ArrayList要高 因为ArrayList每次扩容的容量是当前的1.5倍,因此LinkedList所占的内存空间要更小一些 两者的遍历效率接近,但须要注意,遍历LinkedList时应用iterator方式,不要用get(int)方式,不然效率会很低 Vector
Vector和ArrayList很像,都是基于数组实现的集合,它和ArrayList的主要区别在于
Vector是线程安全的,而ArrayList不是 因为Vector中的方法基本都是synchronized的,其性能低于ArrayList Vector能够定义数组长度扩容的因子,ArrayList不能 CopyOnWriteArrayList
与 Vector同样,CopyOnWriteArrayList也能够认为是ArrayList的线程安全版,不一样之处在于 CopyOnWriteArrayList在写操做时会先复制出一个副本,在新副本上执行写操做,而后再修改引用。这种机制让 CopyOnWriteArrayList能够对读操做不加锁,这就使CopyOnWriteArrayList的读效率远高于Vector。 CopyOnWriteArrayList的理念比较相似读写分离,适合读多写少的多线程场景。但要注意,CopyOnWriteArrayList只能保证数据的最终一致性,并不能保证数据的实时一致性,若是一个写操做正在进行中且并未完成,此时的读操做没法保证能读到这个写操做的结果。
Vector vs CopyOnWriteArrayList
两者均是线程安全的、基于数组实现的List Vector是【绝对】线程安全的,CopyOnWriteArrayList只能保证读线程会读到【已完成】的写结果,但没法像Vector同样实现读操做的【等待写操做完成后再读最新值】的能力 CopyOnWriteArrayList读性能远高于Vector,并发线程越多优点越明显 CopyOnWriteArrayList占用更多的内存空间 Map类集合
图片.png
Map将key和value封装至一个叫作Entry的对象中,Map中存储的元素实际是Entry。只有在keySet()和values()方法被调用时,Map才会将keySet和values对象实例化。
每个Map根据其自身特色,都有不一样的Entry实现,以对应Map的内部类形式出现。
前文已经对Map接口的基本特色进行过描述,咱们直接来看一下Map接口的经常使用实现类
HashMap
HashMap将Entry对象存储在一个数组中,并经过哈希表来实现对Entry的快速访问:
图片.png
由每一个Entry中的key的哈希值决定该Entry在数组中的位置。以这种特性可以实现经过key快速查找到Entry,从而得到该key对应的value。在不发生哈希冲突的前提下,查找的时间复杂度是O(1)。
若是两个不一样的key计算出的index是同样的,就会发生两个不一样的key都对应到数组中同一个位置的状况,也就是所谓的哈希冲突。HashMap处理哈 希冲突的方法是拉链法,也就是说数组中每一个位置保存的实际是一个Entry链表,链表中每一个Entry都拥有指向链表中后一个Entry的引用。在发生哈希冲突时,将冲突的Entry追加至链表的头部。当HashMap在寻址时发现某个key对应的数组index上有多个Entry,便会遍历该位置上的 Entry链表,直到找到目标的Entry。
图片.png
HashMap的Entry类:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; } HashMap因为其快速寻址的特色,能够说是最常常被使用的Map实现类
Hashtable
Hashtable 能够说是HashMap的前身(Hashtable自JDK1.0就存在,而HashMap乃至整个Map接口都是JDK1.2引入的新特性),其实现思 路与HashMap几乎彻底同样,都是经过数组存储Entry,以key的哈希值计算Entry在数组中的index,用拉链法解决哈希冲突。两者最大的不一样在于,Hashtable是线程安全的,其提供的方法几乎都是同步的。
ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全版(自JDK1.5引入),提供比Hashtable更高效的并发性能。
图片.png
Hashtable 在进行读写操做时会锁住整个Entry数组,这就致使数据越多性能越差。而ConcurrentHashMap使用分离锁的思路解决并发性能,其将 Entry数组拆分至16个Segment中,以哈希算法决定Entry应该存储在哪一个Segment。这样就能够实如今写操做时只对一个Segment 加锁,大幅提高了并发写的性能。
在进行读操做时,ConcurrentHashMap在绝大部分状况下都不须要加锁,其Entry中的value是volatile的,这保证了value被修改时的线程可见性,无需加锁便能实现线程安全的读操做。
ConcurrentHashMap的HashEntry类:
static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; } 可是鱼与熊掌不可兼得,ConcurrentHashMap的高性能是有代价的(不然Hashtable就没有存在价值了),那就是它不能保证读操做的绝对 一致性。ConcurrentHashMap保证读操做能获取到已存在Entry的value的最新值,同时也能保证读操做可获取到已完成的写操做的内容,但若是写操做是在建立一个新的Entry,那么在写操做没有完成时,读操做是有可能获取不到这个Entry的。
HashMap vs Hashtable vs ConcurrentHashMap
三者在数据存储层面的机制原理基本一致 HashMap不是线程安全的,多线程环境下除了不能保证数据一致性以外,还有可能在rehash阶段引起Entry链表成环,致使死循环 Hashtable是线程安全的,能保证绝对的数据一致性,但性能是问题,并发线程越多,性能越差 ConcurrentHashMap 也是线程安全的,使用分离锁和volatile等方法极大地提高了读写性能,同时也能保证在绝大部分状况下的数据一致性。但其不能保证绝对的数据一致性, 在一个线程向Map中加入Entry的操做没有彻底完成以前,其余线程有可能读不到新加入的Entry LinkedHashMap
LinkedHashMap与HashMap很是相似,惟一的不一样在于前者的Entry在HashMap.Entry的基础上增长了到前一个插入和后一个插入的Entry的引用,以实现可以按Entry的插入顺序进行遍历。
图片.png
TreeMap
TreeMap是基于红黑树实现的Map结构,其Entry类拥有到左/右叶子节点和父节点的引用,同时还记录了本身的颜色:
static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left = null; Entry<K,V> right = null; Entry<K,V> parent; boolean color = BLACK; } 红黑树实际是一种算法复杂但高效的平衡二叉树,具有二叉树的基本性质,即任何节点的值大于其左叶子节点,小于其右叶子节点,利用这种特性,TreeMap可以实现Entry的排序和快速查找。
关于红黑树的具体介绍,能够参考这篇文章,很是详细:blog.csdn.net/chenssy/art…
TreeMap的Entry是有序的,因此提供了一系列方便的功能,好比获取以升序或降序排列的KeySet(EntrySet)、获取在指定key(Entry)以前/以后的key(Entry)等等。适合须要对key进行有序操做的场景。
ConcurrentSkipListMap
ConcurrentSkipListMap一样可以提供有序的Entry排列,但其实现原理与TreeMap不一样,是基于跳表(SkipList)的:
图片.png
如上图所示,ConcurrentSkipListMap由一个多级链表实现,底层链上拥有全部元素,逐级上升的过程当中每一个链的元素数递减。在查找时从顶层链出发,按先右后下的优先级进行查找,从而实现快速寻址。
static class Index<K,V> { final Node<K,V> node; final Index<K,V> down;//下引用 volatile Index<K,V> right;//右引用 } 与TreeMap不一样,ConcurrentSkipListMap在进行插入、删除等操做时,只须要修改影响到的节点的右引用,而右引用又是volatile的,因此ConcurrentSkipListMap是线程安全的。但ConcurrentSkipListMap与ConcurrentHashMap同样,不能保证数据的绝对一致性,在某些状况下有可能没法读到正在被插入的数据。
TreeMap vs ConcurrentSkipListMap
两者都可以提供有序的Entry集合 两者的性能相近,查找时间复杂度都是O(logN) ConcurrentSkipListMap会占用更多的内存空间 ConcurrentSkipListMap是线程安全的,TreeMap不是 Set类集合 Set 接口继承Collection,用于存储不含重复元素的集合。几乎全部的Set实现都是基于同类型Map的,简单地说,Set是阉割版的Map。每个Set内都有一个同类型的Map实例(CopyOnWriteArraySet除外,它内置的是CopyOnWriteArrayList实例),Set把元素做为key存储在本身的Map实例中,value则是一个空的Object。Set的经常使用实现也包括 HashSet、TreeSet、ConcurrentSkipListSet等,原理和对应的Map实现彻底一致,此处再也不赘述。
图片.png
Queue/Deque类集合
图片.png
Queue和Deque接口继承Collection接口,实现FIFO(先进先出)的集合。两者的区别在于,Queue只能在队尾入队,队头出队,而Deque接口则在队头和队尾均可以执行出/入队操做
Queue接口经常使用方法:
add(E)/offer(E):入队,即向队尾追加元素,两者的区别在于若是队列是有界的,add方法在队列已满的状况下会抛出IllegalStateException,而offer方法只会返回false remove()/poll():出队,即从队头移除1个元素,两者的区别在于若是队列是空的,remove方法会抛出NoSuchElementException,而poll只会返回null element()/peek():查看队头元素,两者的区别在于若是队列是空的,element方法会抛出NoSuchElementException,而peek只会返回null Deque接口经常使用方法:
addFirst(E) / addLast(E) / offerFirst(E) / offerLast(E) removeFirst() / removeLast() / pollFirst() / pollLast() getFirst() / getLast() / peekFirst() / peekLast() removeFirstOccurrence(Object) / removeLastOccurrence(Object) Queue接口的经常使用实现类:
ConcurrentLinkedQueue
ConcurrentLinkedQueue是基于链表实现的队列,队列中每一个Node拥有到下一个Node的引用:
private static class Node { volatile E item; volatile Node next; } 因为Node类的成员都是volatile的,因此ConcurrentLinkedQueue天然是线程安全的。可以保证入队和出队操做的原子性和一致性,但在遍历和size()操做时只能保证数据的弱一致性。
LinkedBlockingQueue
与ConcurrentLinkedQueue不一样,LinkedBlocklingQueue是一种无界的阻塞队列。所谓阻塞队列,就是在入队时若是队列已满,线程会被阻塞,直到队列有空间供入队再返回;同时在出队时,若是队列已空,线程也会被阻塞,直到队列中有元素供出队时再返回。LinkedBlocklingQueue一样基于链表实现,其出队和入队操做都会使用ReentrantLock进行加锁。因此自己是线程安全的,但一样的,只能保证入队和出队操做的原子性和一致性,在遍历时只能保证数据的弱一致性。
ArrayBlockingQueue
ArrayBlockingQueue是一种有界的阻塞队列,基于数组实现。其同步阻塞机制的实现与LinkedBlocklingQueue基本一致,区别仅在于前者的生产和消费使用同一个锁,后者的生产和消费使用分离的两个锁。
ConcurrentLinkedQueue vsLinkedBlocklingQueue vs ArrayBlockingQueue
ConcurrentLinkedQueue是非阻塞队列,其余二者为阻塞队列 三者都是线程安全的 LinkedBlocklingQueue是无界的,适合实现不限长度的队列, ArrayBlockingQueue适合实现定长的队列 SynchronousQueue
SynchronousQueue算是JDK实现的队列中比较奇葩的一个,它不能保存任何元素,size永远是0,peek()永远返回null。向其中插入元素的线程会阻塞,直到有另外一个线程将这个元素取走,反之从其中取元素的线程也会阻塞,直到有另外一个线程插入元素。
这种实现机制很是适合传递性的场景。也就是说若是生产者线程须要及时确认到本身生产的任务已经被消费者线程取走后才能执行后续逻辑的场景下,适合使用SynchronousQueue。
PriorityQueue & PriorityBlockingQueue
这两种Queue并非FIFO队列,而是根据元素的优先级进行排序,保证最小的元素最早出队,也能够在构造队列时传入Comparator实例,这样PriorityQueue就会按照Comparator实例的要求对元素进行排序。
PriorityQueue是非阻塞队列,也不是线程安全的,PriorityBlockingQueue是阻塞队列,同时也是线程安全的。
Deque 的实现类包括LinkedList(前文已描述过)、ConcurrentLinkedDeque、LinkedBlockingDeque,其实现机制与前文所述的ConcurrentLinkedQueue和LinkedBlockingQueue很是相似,此处再也不赘述
最后,对本文中描述的经常使用集合实现类作一个简单总结:
喜欢的点点关注,点点赞。 对Java技术,架构技术感兴趣的同窗,欢迎加QQ群668041364?,一块儿学习,相互讨论。
群内已经有小伙伴将知识体系整理好(源码,笔记,PPT,学习视频),欢迎加群领取。