Java集合类总结

前言

以前一直作C++开发,在使用标准集合类的类库时都是使用的STL,觉的这个就是比C语言很是大的进步,很好用;后来玩Java,发现Java中的集合类更是好用,可是因为Java语言的发展缘由,在使用的过程当中也有不少坑,有不少的细节须要去处理。最近在进行组内代码评审时,就发现开发人员乱用集合类的状况。不少开发人员就不明白各个集合类的特性和使用场景,反正列表就用ArrayList,键值就用HashMap,仿佛在他们眼中Java的集合类就只有ArrayListHashMap这两种。不怕你们笑话,曾经我也是这么使用的,今天就用一点时间,好好的对Java集合类的使用进行一次扫盲。html

Java集合概述

Java提供的众多集合类由两大接口衍生而来:Collection接口和Map接口。为了更好的把握Java集合类的总体结构,我这里先贴一个Java集合的总体类图,以便你们对Java集合类有一个总体的印象。java

乍一看这个图很复杂,其实咱们仔细梳理一下,这个图仍是很是清晰的。能够这么看,在Java的集合类中,主要分为ListMapSetQueue这四大类,这四大接口类下面,又根据使用场景分为多个具体的子类。下面就一一进行总结。算法

Collection接口说明

从类图上能够看到,Collection接口做为一个很是重要的基础接口,因此咱们有必要对Collection接口中的经常使用方法进行一下说明和总结:数组

  • add:向集合中添加单个元素
  • addAll:向集合中批量添加元素
  • clear:删除集合中全部元素
  • contains:判断集合是否包含某个元素
  • isEmpty:判断集合是否为空
  • iterator:返回一个集合迭代器;关于迭代器能够参考这篇《Java中的Enumeration、Iterable和Iterator接口详解
  • remove:从集合中删除单个元素
  • removeAll:从集合中批量删除元素
  • retainAll:保留指定入参集合中的元素,删除其它元素
  • size:获取集合中元素个数
  • toArray:将集合转换为数组

Map接口说明

一样的,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类的集合出镜频率很是高,对于List类的集合,咱们须要知道经常使用的有ArrayListLinkedListVectorCopyOnWriteArrayList,特别是ArrayListCopyOnWriteArrayList这两货,更是频繁出镜。并发

  • 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等)时,都会将原数组拷贝一份,而后在新数组上作修改操做。最后集合的引用指向新数组。CopyOnWriteArrayListVector都是线程安全的,不一样的是:前者使用ReentrantLock类,后者使用synchronized关键字。ReentrantLock提供了更多的锁投票机制,在锁竞争的状况下能表现更佳的性能。就是它让JVM能更快的调度线程,才有更多的时间去执行线程。这就是为何CopyOnWriteArrayList的性能在大并发量的状况下优于Vector的缘由。

    对于CopyOnWriteArrayList来讲,很是适合高并发的读操做(读多写少)的场景下使用。若写的操做很是多,会频繁复制容器,从而影响性能。

Map

Map存储的是键值对,它将key和value封装至一个叫作Entry的对象中。每个Map根据其自身的特色,都有不一样的Entry实现,以对应Map的内部类形式出现。

根据我如今的开发状况来看,MapList类的集合更经常使用。对于Map类的集合有HashMapHashTableSortedMapTreeMapWeakHashMapConcurrentSkipListMap

  • HashMap
    HashMap的底层是基于数组+链表+红黑树(JDK1.8+)的方式实现的。HashMapEntry对象存储在一个数组中,并经过哈希表来实现对Entry的快速访问。感受这里不放一张图,就不能更好的理解HashMap的实现方式了:

    经过上图你们应该有一个总体的理解,我这里也不会对HashMap的实现原理进行更进一步的剖析。若是对HashMap的实现源码感兴趣,能够阅读《一文让你完全理解 Java HashMap 和 ConcurrentHashMap》和《Java集合,HashMap底层实现和原理(1.7数组+链表与1.8+的数组+链表+红黑树)》这两篇文章。对于HashMap的一些特性这里进行列举:
    • 当储存对象时,咱们将键值对传递给put(key,value)方法时,它调用键对象key的hashCode()方法来计算hashcode,而后找到bucket位置,来储存值对象value
    • hash表里能够存储元素的位置称为桶(bucket),若是经过key计算hash值发生冲突时,那么将采用链表的形式,来存储元素
    • HashMap的扩容操做是一项很耗时的任务,因此若是能估算Map的容量,最好给它一个默认初始值,避免进行屡次扩容;当数量达到了16 * 0.75 = 12就须要将当前16的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操做,因此很是消耗性能
    • 容许使用null建和null
    • 非线程安全
  • HashTable
    HashTableHashMap的线程安全版,Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步。对于HashTable这种上古的东西,在开发中不建议使用了,由于如今已经提供了ConcurrentHashMap来使用。

  • ConcurrentHashMap
    ConcurrentHashMapHashMap的线程安全版(自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用于存储不含重复元素的集合,几乎全部的Set实现都是基于同类型Map的。简单地说,Set是阉割版的Map。每个Set内都有一个同类型的Map实例(CopyOnWriteArraySet除外,它内置的是CopyOnWriteArrayList实例),Set把元素做为key存储在本身的Map实例中,value则是一个空的Object。Set的经常使用实现包括HashSetTreeSetConcurrentSkipListSet,因为实现原理和对应的Map是彻底一致的,因此这里就再也不赘述。

在实际评审代码中,发现开发人员不多用Set类型的集合,即便有存储不含重复元素的场景,也都是使用ArrayList集合,而后结合着contains这种奇葩方式来实现。也就是说,一些基本功不扎实的开发人员,在脑海中就没有Set集合的概念。抱着实现功能就OK的心态,管他代码质量好很差,全凭ArrayListHashMap闯天下。

Queue

Queue用于模拟“队列”这种数据结构(先进先出FIFO)。队列的头部保存着队列中存放时间最长的元素,队列的尾部保存着队列中存放时间最短的元素。新元素插入到队列的尾部。这种队列基本都只是在小数据量的状况下使用,对于互联网应用来讲,基本都是在使用分布式消息队列中间件。从文章开头的类图中能够看出,Deque接口继承了Queue接口,Deque接口表明一个“双端队列”,双端队列能够同时从两端来添加、删除元素,所以Deque的实现类既能够当成队列使用、也能够当成栈使用。对于咱们来讲,经常使用的Queue实现类有ArrayDequeConcurrentLinkedQueueLinkedBlockingQueueArrayBlockingQueueSynchronousQueuePriorityQueuePriorityBlockingQueue

  • 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(前文已经总结过)、ConcurrentLinkedDequeLinkedBlockingDeque,其实现机制与上面所述的ConcurrentLinkedQueueLinkedBlockingQueue很是相似,此处再也不赘述。

总结

这里对Java中的一些经常使用集合类进行了大概原理性的总结,并无深刻到源码级别,若是深刻到源码级别,那就够讲一本书的了,并且花费的精力和时间也太大了,这里就是浅尝辄止,有个基本的了解便可。了解原理,对本身写的代码负责。

2019年8月11日 于内蒙古呼和浩特。

相关文章
相关标签/搜索