以前一直作C++开发,在使用标准集合类的类库时都是使用的STL,觉的这个就是比C语言很是大的进步,很好用;后来玩Java,发现Java中的集合类更是好用,可是因为Java语言的发展缘由,在使用的过程当中也有不少坑,有不少的细节须要去处理。最近在进行组内代码评审时,就发现开发人员乱用集合类的状况。不少开发人员就不明白各个集合类的特性和使用场景,反正列表就用ArrayList
,键值就用HashMap
,仿佛在他们眼中Java的集合类就只有ArrayList
和HashMap
这两种。不怕你们笑话,曾经我也是这么使用的,今天就用一点时间,好好的对Java集合类的使用进行一次扫盲。html
Java提供的众多集合类由两大接口衍生而来:Collection
接口和Map
接口。为了更好的把握Java集合类的总体结构,我这里先贴一个Java集合的总体类图,以便你们对Java集合类有一个总体的印象。java
乍一看这个图很复杂,其实咱们仔细梳理一下,这个图仍是很是清晰的。能够这么看,在Java的集合类中,主要分为List
、Map
、Set
和Queue
这四大类,这四大接口类下面,又根据使用场景分为多个具体的子类。下面就一一进行总结。算法
从类图上能够看到,Collection
接口做为一个很是重要的基础接口,因此咱们有必要对Collection
接口中的经常使用方法进行一下说明和总结:数组
add
:向集合中添加单个元素addAll
:向集合中批量添加元素clear
:删除集合中全部元素contains
:判断集合是否包含某个元素isEmpty
:判断集合是否为空iterator
:返回一个集合迭代器;关于迭代器能够参考这篇《Java中的Enumeration、Iterable和Iterator接口详解》remove
:从集合中删除单个元素removeAll
:从集合中批量删除元素retainAll
:保留指定入参集合中的元素,删除其它元素size
:获取集合中元素个数toArray
:将集合转换为数组一样的,Map
接口做为很是重要的接口,也有必要对其中的一些重要方法进行一些说明:安全
clear
:删除全部元素containsKey
:判断是否包含某个键containsValue
:判断是否包含某个值entrySet
:将Map键值对以Map.Entry的形式放入Set集合中返回get
:返回key值所对应的对象isEmpty
:判断是否为空keySet
:返回全部键的Set集合,这里有一篇文章《JAVA中Map使用keySet()和entrySet()进行遍历效率的对比》能够看一看put
:向Map中添加单个元素putAll
:向Map中批量添加元素remove
:删除Key所对应的对象size
:获取Map中键值对的个数values
:返回全部值的集合说完这两大经常使用接口的经常使用方法,下面就对这两大接口衍生出来的经常使用集合类进行说明和总结。数据结构
List
用于定义以列表形式存储的集合,List
接口为集合中的每一个对象分配了一个索引,用来标记该对象在List中的位置,并能够经过索引定位到指定位置的对象。多线程
在咱们开发过程当中,List
类的集合出镜频率很是高,对于List
类的集合,咱们须要知道经常使用的有ArrayList
、LinkedList
、Vector
、CopyOnWriteArrayList
,特别是ArrayList
和CopyOnWriteArrayList
这两货,更是频繁出镜。并发
ArrayList
ArrayList
基于数组实现的非线程安全的集合,在内部实现上,其维护了一个可变长度的对象数组,集合内全部对象存储于这个数组中,并实现该数组长度的动态伸缩。知道了内部的实现原理,那对于ArrayList
来讲,就有如下几个特性:
若是涉及到频繁的插入和删除元素,ArrayList
则不是最好的选择。分布式
LinkedList
LinkedList
基于链表实现的非线程安全的集合,在内部实现上,其实现了静态类Node,集合中的每一个对象都由一个Node保存,每一个Node都拥有到本身的前一个和后一个Node引用。对于LinkedList
来讲,它具有如下特性:高并发
LinkedList
时应用iterator方式,不要用get(int)方式,不然效率会很低Vector
基于数组实现的线程安全的集合。线程同步(方法被synchronized
修饰),性能比ArrayList
差。当并发量增多时,锁竞争的问题严重,会致使性能降低。
CopyOnWriteArrayList
与Vector
同样,CopyOnWriteArrayList
也能够认为是ArrayList
的线程安全版,不一样之处在于 CopyOnWriteArrayList
在写操做时会先复制出一个副本,在新副本上执行写操做,而后再修改引用。这种机制让CopyOnWriteArrayList
能够对读操做不加锁,这就使CopyOnWriteArrayList
的读效率远高于Vector。 CopyOnWriteArrayList
的理念比较相似读写分离,适合读多写少的多线程场景。但要注意,CopyOnWriteArrayList
只能保证数据的最终一致性,并不能保证数据的实时一致性,若是一个写操做正在进行中且并未完成,此时的读操做没法保证能读到这个写操做的结果。
CopyOnWriteArrayList
写时复制的集合,在执行写操做(如:add,set,remove等)时,都会将原数组拷贝一份,而后在新数组上作修改操做。最后集合的引用指向新数组。CopyOnWriteArrayList
和Vector
都是线程安全的,不一样的是:前者使用ReentrantLock
类,后者使用synchronized
关键字。ReentrantLock
提供了更多的锁投票机制,在锁竞争的状况下能表现更佳的性能。就是它让JVM能更快的调度线程,才有更多的时间去执行线程。这就是为何CopyOnWriteArrayList
的性能在大并发量的状况下优于Vector
的缘由。
对于CopyOnWriteArrayList
来讲,很是适合高并发的读操做(读多写少)的场景下使用。若写的操做很是多,会频繁复制容器,从而影响性能。
Map
存储的是键值对,它将key和value封装至一个叫作Entry的对象中。每个Map根据其自身的特色,都有不一样的Entry实现,以对应Map的内部类形式出现。
根据我如今的开发状况来看,Map
比List
类的集合更经常使用。对于Map
类的集合有HashMap
、HashTable
、SortedMap
、TreeMap
、WeakHashMap
和ConcurrentSkipListMap
。
HashMap
HashMap
的底层是基于数组+链表+红黑树
(JDK1.8+)的方式实现的。HashMap
将Entry
对象存储在一个数组中,并经过哈希表来实现对Entry
的快速访问。感受这里不放一张图,就不能更好的理解HashMap
的实现方式了:
HashMap
的实现原理进行更进一步的剖析。若是对HashMap
的实现源码感兴趣,能够阅读《一文让你完全理解 Java HashMap 和 ConcurrentHashMap》和《Java集合,HashMap底层实现和原理(1.7数组+链表与1.8+的数组+链表+红黑树)》这两篇文章。对于HashMap
的一些特性这里进行列举:
null
建和null
值HashTable
HashTable
是HashMap
的线程安全版,Hashtable
的实现方法里面都添加了synchronized
关键字来确保线程同步。对于HashTable
这种上古的东西,在开发中不建议使用了,由于如今已经提供了ConcurrentHashMap
来使用。
ConcurrentHashMap
ConcurrentHashMap
是HashMap
的线程安全版(自JDK1.5引入),提供比Hashtable
更高效的并发性能。
HashTable
在进行读写操做时会锁住整个Entry数组,这就致使数据越多性能越差。而ConcurrentHashMap
使用分离锁的思路解决并发性能,其将Entry数组拆分至16个Segment中,以哈希算法决定Entry应该存储在哪一个Segment。这样就能够实如今写操做时只对一个Segment加锁,大幅提高了并发写的性能。在进行读操做时,ConcurrentHashMap
在绝大部分状况下都不须要加锁,其Entry中的value是volatile的,这保证了value被修改时的线程可见性,无需加锁便能实现线程安全的读操做。
ConcurrentHashMap
采用了分段锁技术,其中Segment继承于ReentrantLock。不会像HashTable
那样不论是put仍是get操做都须要作同步处理,理论上ConcurrentHashMap
支持 CurrencyLevel (Segment数组数量)的线程并发。每当一个线程占用锁访问一个Segment时,不会影响到其余的Segment。
Set
用于存储不含重复元素的集合,几乎全部的Set实现都是基于同类型Map的。简单地说,Set是阉割版的Map。每个Set内都有一个同类型的Map实例(CopyOnWriteArraySet
除外,它内置的是CopyOnWriteArrayList
实例),Set把元素做为key存储在本身的Map实例中,value则是一个空的Object。Set
的经常使用实现包括HashSet
、TreeSet
和ConcurrentSkipListSet
,因为实现原理和对应的Map是彻底一致的,因此这里就再也不赘述。
在实际评审代码中,发现开发人员不多用Set
类型的集合,即便有存储不含重复元素的场景,也都是使用ArrayList
集合,而后结合着contains
这种奇葩方式来实现。也就是说,一些基本功不扎实的开发人员,在脑海中就没有Set
集合的概念。抱着实现功能就OK的心态,管他代码质量好很差,全凭ArrayList
和HashMap
闯天下。
Queue
用于模拟“队列”这种数据结构(先进先出FIFO)。队列的头部保存着队列中存放时间最长的元素,队列的尾部保存着队列中存放时间最短的元素。新元素插入到队列的尾部。这种队列基本都只是在小数据量的状况下使用,对于互联网应用来讲,基本都是在使用分布式消息队列中间件。从文章开头的类图中能够看出,Deque
接口继承了Queue
接口,Deque
接口表明一个“双端队列”,双端队列能够同时从两端来添加、删除元素,所以Deque
的实现类既能够当成队列使用、也能够当成栈使用。对于咱们来讲,经常使用的Queue
实现类有ArrayDeque
、ConcurrentLinkedQueue
、LinkedBlockingQueue
、ArrayBlockingQueue
、SynchronousQueue
、PriorityQueue
和PriorityBlockingQueue
。
ArrayDeque
是一个基于数组的双端队列,和ArrayList
相似,它们的底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出该数组的容量时,系统会在底层从新分配一个Object[]数组来存储集合元素。
ConcurrentLinkedQueue
ConcurrentLinkedQueue
是基于链表实现的线程安全、无界非阻塞队列,队列中每一个Node拥有到下一个Node的引用。它可以保证入队和出队操做的原子性和一致性,但在遍历和size()操做时只能保证数据的弱一致性。
LinkedBlockingQueue
与ConcurrentLinkedQueue
不一样,LinkedBlocklingQueue
是一种无界的阻塞队列。所谓阻塞队列,就是在入队时若是队列已满,线程会被阻塞,直到队列有空间供入队再返回;同时在出队时,若是队列已空,线程也会被阻塞,直到队列中有元素供出队时再返回。LinkedBlocklingQueue
一样基于链表实现,其出队和入队操做都会使用ReentrantLock进行加锁。因此自己是线程安全的,但一样的,只能保证入队和出队操做的原子性和一致性,在遍历时只能保证数据的弱一致性。
ArrayBlockingQueue
ArrayBlockingQueue
是一种有界的阻塞队列,基于数组实现。其同步阻塞机制的实现与LinkedBlocklingQueue
基本一致,区别仅在于前者的生产和消费使用同一个锁,后者的生产和消费使用分离的两个锁。
SynchronousQueue
SynchronousQueue
算是JDK实现的队列中比较奇葩的一个,它不能保存任何元素,size永远是0,peek()永远返回null。向其中插入元素的线程会阻塞,直到有另外一个线程将这个元素取走,反之从其中取元素的线程也会阻塞,直到有另外一个线程插入元素。这种实现机制很是适合传递性的场景。也就是说若是生产者线程须要及时确认到本身生产的任务已经被消费者线程取走后才能执行后续逻辑的场景下,适合使用SynchronousQueue
。
PriorityQueue
PriorityQueue
是基于最小堆数据结构,能够在构造时指定Comparator
或者按照天然顺序排序。优先队列有最大优先队列和最小优先队列,分别由最大堆和最小堆实现。PriorityQueue
是非阻塞队列,也不是线程安全的。
PriorityBlockingQueue
PriorityBlockingQueue
实现原理同PriorityQueue
同样,可是PriorityBlockingQueue
是阻塞队列,同时也是线程安全的。
Deque
的实现类包括LinkedList
(前文已经总结过)、ConcurrentLinkedDeque
和LinkedBlockingDeque
,其实现机制与上面所述的ConcurrentLinkedQueue
和LinkedBlockingQueue
很是相似,此处再也不赘述。
这里对Java中的一些经常使用集合类进行了大概原理性的总结,并无深刻到源码级别,若是深刻到源码级别,那就够讲一本书的了,并且花费的精力和时间也太大了,这里就是浅尝辄止,有个基本的了解便可。了解原理,对本身写的代码负责。
2019年8月11日 于内蒙古呼和浩特。