Java 集合框架中的常用集合及其特点、适用场景、实现原理简介

Java提供的众多集合类由两大接口衍生而来:Collection接口和Map接口

Map接口

Map接口在Collection的基础上,为其中的每个对象指定了一个key,并使用Entry保存每个key-value对,以实现通过key快速定位到对象(value)。Map接口的主要方法包括:

int size();//集合内元素数量
boolean isEmpty();//判断是否为空
boolean containsKey(Object key);//判断Map中是否存在指定 key
boolean containsValue(Object value);//判断Map中是否存在指定 value
V get(Object key);//返回 key 对应的对象
V put(K key, V value);//向Map内添加对象
V remove(Object key);//删除 key 对应的对象
void putAll(Map<? extends K, ? extends V> m);//批量添加
void clear();//清空集合
Set<K> keySet();//返回Map中所有的 key
Collection<V> values();返回集合中所有的 value
Set<Map.Entry<K, V>> entrySet();//返回Map中所有key-value 对应的EntrySet
interface Entry<K,V> {...};//内部实现Enntry接口,实际用Entry存储
boolean equals(Object o);
int hashCode();
//jdk 1.8
default V getOrDefault(Object key, V defaultValue);
//集合中有key对应的 vlaue时返回 value,否则否则返回默认值(defaultValue)
  • Map类集合

  • Map将Key和value封装至一个叫Entry的对象中,Map中存储的元素实际是Entry。

  • 只有在KeySet()和values()方法被调用时,Map才会将KeySet和values对象实例化。

  • 每个Map根据其自身特点,都有不同的Entry实现,以应对Map的内部类形式出现。

HashMap

HashMap将Entry对象存储在一个数组中,并通过哈希表来实现对Entry的快速访问:

index = hash & (length-1) 

当满足 length 为 2 的次幂时,会满足一个公式:hash & (length-1) = hash%n
减少哈希碰撞

image-20200316192833103

不发生哈希碰撞:查找时间复杂度为 O(1)

由每个Entry中的key的哈希值决定该Entry在数组中的位置,以这种特性能够实现通过key快速查找到Entry,从而获得该key对应的value,在不发生哈希冲突的前提下,查找的时间复杂度时O(1),

不发生哈希碰撞:查找时间复杂度为与链长度有关

如果两个不同的key计算出的index是一样的,就会发生两个不同的key都对应到数组中同一个位置的的情况,也就是说所谓的哈希冲突。

HashMap处理哈希冲突的方法是拉链法,也就是说数组中每个位置保存的实际是一个Entry链表,链表中每个Entry都拥有指向链表中后一个Entry的引用,在发生哈希冲突时,将冲突的Entry追加至链表的头部。当HashMap在寻址时发现某个Key对应的数组index上有多个Entry,便会遍历该位置上的Entry链表(红黑树),直到找到目标的Entry。

image-20200316192821285

HashMap的Entry类(内部类):

image-20200316192812236

JDK 1.8

链表长度达到 64 则会树化,达到 6 时则会链化

HashMap由于其快速寻址的特点,可以说是最经常被使用的Map实现类

Hashtable(线程安全,性能较差)

Hashtable可以说时HaskMap的前身,(Hashtable自JDK1.0就存在,而HashMap乃至整个Map接口都是JDK1.2引入的新特性),其实现思路与HashMap几乎完全一样,都是通过数组存储Entry,以Key的哈希值计算Entry在数组中的index,用拉链法解决哈希冲突。二者最大的不同在于,Hashtable是线程安全的,其提供的方法几乎都是同步的。

ConcurrentHashMap

ConcurrentHashMap是HashMap的线程安全版(自JDK1.6引入),提供比Hashtable更高效的并发性能。

image-20200316192758824
Hashtable在进行读写操作时会锁住整个Entry数组,这就导致数据越多性能越差。而ConcurrenHashMap使用分离锁思路解决并发性能,其将Entry数组拆分至16个Segment中,以哈希算法决定Entry应该存储在哪个Segment,这样就可以实现在写操作时只对一个Segment加锁,大幅提升了并发写的性能。

在进行读操作时,ConcurrentHashMap在绝大部分情况下都不需要加锁,其中Entry中的value是volatile的,这就保证了value被修改是的线程可见性,无需加锁便能实现线程安全的读操作。

ConcurrentHashMap的HashEntry类:

image-20200316192749325

但是鱼与熊掌不可兼得,ConcurrentHashMap的高性能是有代价的(否则Hash table就没有存在的价值了),那就是它不能保证读操作的绝对一致性,ConcurrHashMap保证读操作能获取到已存在的Entry和value的最新值,同时也能保证读操作可以获取到已完成的写操作的内容,但是如果写操作是在创建一个新的Entry,那么在写操作没有完成时,读操作是有可能获取不到这个Entry的。

HashMap VS Hashtable VS ConcurrentHashMap

  • 三者在数据存储层面的机制原理基本一致
  • HashMap不是线程安全的,多线程环境下除了不能保证数据一致性外,还有可能在rehash阶段引发Entry链表成环,导致死循环。
  • Hashtable是线程安全的,能保证觉得的数据一致性,但性能是问题,并发线程越多,性能越差。
  • ConcurrentHashMap 也是线程安全的,使用分离锁和volatile等方法极大的提升了读写性能,同时也能保证绝大部分情况下的数据一致性。但其不能保证绝对的都数据一致性,在一个线程向Map中加入Entry的操作没有完全完成之前,其他线程有可能读取不到新加入的Entry。

LinkedHashMap

LinkedhashMap与HashMap非常类似,唯一的不同在于前者的Entry在HashMap.Entey的基础上增加了到前一个插入和后一个插入的entry的应用,以实现能够按Entry的插入顺序进行遍历。

image-20200316192737056
image-20200316192727549

TreeMap

TreeMap是基于红黑树实现的Map结构,其Entry类拥有到左 / 右叶子节点和父节点的引用,同时还记录了自己的颜色:

image-20200316192713754

红黑树实际上是一种算法复杂但高效的平衡二叉树,具备二叉树的基本性质,即任何节点的值大于其左叶子节点小于其右子节点,利用这种特性,TreeMap能够实现Entry的排序和快速查找。

TreeMap的Entry是有序的,所以提供了一系列方便的功能,比如获取以升序或降序排列的KeySet(EntrySet),获取指定key(Entry)之前 / 之后的key(Entry)等等。适合需要对key进行有序操作的场景。

ConcurrentSkipListMap

concurrentSkipListMap同样能够提供有序的Entry排列,但其实现原理与TreeMap不同,是基于跳表(Skip List)的:

image-20200316192659767

如上图所示,ConcurrentSkipListMap由一个多级链表实现,底层链上拥有所有元素,逐级上升的过程中每个链的元素递减。在查找时从顶层链出发,按先右后下的优先级进行查找,从而实现快速寻址。

image-20200316192646683

与TreeMap不同,ConcurrentSkipListMap在进行插入,删除等操作时,只需要修改影响到的节点的右引用,而右引用又是volatile的,所以ConcurrentSkipListMap是线程安全的。但ConcurrentSkipListMap与ConcurrentHashMap一样,不能保证数据的觉得一致性,在某些情况下有可能无法读到正在被插入的数据。

Tree Map VS ConcurrentSkipListMap

  • 二者都能够提供有序的Entry集合
  • 二者的性能相近,查找时间复杂度都是O(logN)
  • ConcurrentSkipListMap会占用更多的内存空间
  • ConcurrentSkipListMap是线程安全的,TreeMap不是

Collection接口

Collection接口定义了一个包含一批对象的集合。接口的主要方法包括:

int size();//- 集合内的对象数量
boolean isEmpty();//集合是否为空
boolean contains(Object o);//判断集合中是否存在某个元素
Iterator<E> iterator();//返回迭代器
Object[] toArray();//返回集合内的对象数组
boolean add(E e);//添加对象
boolean remove(Object o);//删除对象
boolean containsAll(Collection<?> c);//批量判断集合中某些元素
boolean addAll(Collection<? extends E> c);//批量添加
boolean removeAll(Collection<?> c);//批量删除
boolean retainAll(Collection<?> c);//取交集,原集合被替代
void clear();//清空集合
equals(Object o);
hashCode();

List类集合

List接口继承自Collection,用于定义以列表形式存储的集合,List接口为集合中的每个对象分配了一个索引(index),标记该对象在LIst中的位置

int size();//返回指定 index 位置上的对象
boolean isEmpty();//判断列表是否为空
boolean contains(Object o);//判断列表中是否有该对象
Iterator<E> iterator();//返回迭代器
Object[] toArray();//返回对象数组
boolean add(E e);//添加对象
boolean remove(Object o);//删除对象
boolean containsAll(Collection<?> c);//批量判断列表中是否有该对象
boolean addAll(Collection<? extends E> c);//批量添加对象
boolean addAll(int index, Collection<? extends E> c);//从指定位置批量添加对象
boolean removeAll(Collection<?> c);//批量删除对象
boolean retainAll(Collection<?> c);//取交集,原集合被替代
E get(int index);//获取指定位置对象
E set(int index, E element);//替换指定位置上的对象
int indexOf(Object o);//获取对象对应下标
List<E> subList(int fromIndex, int toIndex);//返回返回指定起始index到终止index的子List对象

List接口的常用实现类

ArrayList

ArrayList基于数组来实现集合的功能,其内部维护了一个可变长的对象数组,集合内所有对象存储于这个数组中,并实现该数组长度的动态伸缩。

每次扩容至原来的1.5倍 即 length>>1

ArrayList使用数组拷贝来实现指定位置的插入和删除:

  • 插入:

image-20200316192612614

  • 删除

image-20200316192547879

LinkedList

LinkedList基于链表来实现集合的功能,其实现了静态类Node,集合中每个对象都由一个Node保存,每个Node都拥有到自己的前一个和后一个Node的引用。

LinkedList追加元素的过程示例:

image-20200316192524925

ArrayList VS LinkedList

  1. ArrayList的随机访问效率更高,基于数组实现的ArrayList可直接定位到目标对象,而LinkedList需要从到头Node或尾Node开始向后 / 向前遍历若干次才能定位到目标i对象
  2. LinkedList在头 / 尾节点执行插入 / 删除操作的效率比ArrayList要高
  3. 由于ArrayList每次扩容的容量是当前的1.5倍,所以LinkedLIst所占用的内存空间要小一些
  4. 二者的遍历效率接近,但需注意,遍历LInkedList时应用iterator方式,不要用get(int)方式,否则效率会很低

Vector

vector和ArrayList很像,都是基于数组实现的集合类,它和ArrayList主要的区别在于

  1. Vector是线程安全的,而ArrayList不是
  2. 由于Vector中的方法基本都是synchronized的,其性能低于ArrayList
  3. Vector可以定义数组长度扩容的因子,ArrayList不能

CopyOnWriteArrayList

  • 与Vector一样,CopyOnWrite ArrayList 也可以认为是ArrayList的线程安全版,不同之处在于CopyOnWriteArrayList在写操作时会先复制出一个副本,在新副本上执行写操作,然后再修改引用,这种机制让CopyOnWriteArrayList可以对读操作不加锁,这就是CopyOnWriteArrayLIst的读效率远高于Vector,CopyOnWriteArrayList的理念比较类似读写分离,适合读多写少的多线程场景,但要注意,CopyOnWriteArrayList只能保证数据的最终一致性,并不能保证数据的实时一致性,如果是一个写操作正在进行中且并未完成,此时的读操作无法保证读到这个写操作的结果。
  • 二者均是线程安全的,基于数组实现的List
  • Vector是绝对线程安全的,CopyOnWriteArrayList只能保证读线程会读到已完成的写结果,但无法像Vector一样实现读操作的等待写操作完成后再读新值的能力
  • CopyOnWriteArrayList读性能远高于Vector,并发线程越多优势越明显
  • CopyOnWriteArrayList占用更多的内存空间

Set类集合

Set接口继承Collection,用于存储不含重复元素的集合。几乎所有的Set实现都是基于同类型的Map,简单的说,Set是阉割版的Map,每一个Set内都有同类型的Map实例(CopyOnWriteArraySet除外,他的内置的是CopyOnWriteArrayList实例),Set把元素作为key存储在自己的Map实例中,value这是一个空的Object。Set的常用实现也包括HashSet,TreeSet,ConcurrentSkipListSet等,原理和对应的Map实现完全一致,此处不再赘述。

image-20200316192506254

Queue / Deque类集合

image-20200316192446623

Queue和Deque接口继承Collection接口,实现FIFO(先进先出)的集合,

二者区别:

  • Queue只能在队尾入队,队头出队
  • Deque接口则在队头和队尾都可以执行出 / 入队操作。

Queue接口常用方法:

  • add(E) / offer(E):入队,即向队尾追加元素,二者的区别在于如果队列是有界的,add方法在队列已满的情况下会抛出IIIegalStateException,而offer方法只会返回false
  • remove() / poll():出队。即从对头移除一个元素,二者的区别在于如果队列是空的,remove方法会抛出NoSuchElementException,而poll只会返回null
  • element() / peek():查看队头元素,二者的区别在于如果队列是空的,element方法会抛出NoSuchElementException,而peek只会返回null

Deque接口常用方法:

  • addFirst(E) / addLast(E) / offerFirst(E) / offeLast(E)
  • removeFirst() / removeLast() / pollFirst() / pollLast(
  • getFirst() / getLast / peekFirst() / peelLast()
  • removeFirstOccurremce(Object) / removeLastOccurrence(Object)

ConcurrentLinkedQueue

ConcurrentLinkedQueue是基于链表实现的队列,队列中每个Node拥有到下一个NOde的引用:

image-20200316192318152

由于Node类的成员都是volatile的,所以ConcurrentLinkedQueue自然是线程安全的。能够保证入队和出队操作的原子性和一致性,但在遍历和size()操作时只能保证数据的弱一致性。

LinkedBlockingQueue

与ConcurrentLinkedQueue不同,LinkedBlockingQueue适合一种无界的阻塞队列,所谓阻塞队列,就是在入队时如果队列已满,线程会被阻塞,直到队列有空间供入队再返回,同时在出队时,如果队列已空,线程也会被阻塞,直到队列中有元素供出队时再返回,

LinkedBlockingQueue同样基于链表实现,其出队和入队操作都会使ReentranLock进行加锁,所有本身是线程安全的,但同样的,只能保证入队和出队的原子性和一致性,在遍历时只能保证数据的弱一致性。

ArrayBlockingQueue

ArrayBlockingQueue是一种有界的阻塞队列,基于数组实现,其同步阻塞机制的实现与LinkedBlockingQueue基本一致,区别仅在于前者的生产和消费使用同一个锁,后者的生产和消费使用分离的两个锁。

ConcurrentLinkedQueue vs LinkedBlockingQueue vs ArrayBlockingQueue

  • ConcurrentLinkedQueue是非阻塞队列,其他两者为阻塞队列
  • 三者都是线程安全的
  • LinkedBlockingQueue是无界的,适合实现不限长度的队列,ArrayBlockQueue适合实现定长的队列

SynchronousQueue

SynchronousQueue算是JDK实现的队列中比较奇葩的一个,它不能保存任何元素,size永远是0,peek()永远返回null。向其中插入元素的线程会阻塞,直到有另一个元素将这个元素取走,反之从其中去元素的线程也会阻塞,直到有另一个元素插入元素。

这种实现机制非常适合传递性的场景,也就是说如果生产者线程需要及时确认到自己生产的任务已经被消费者线程取走后才能执行后续的逻辑的场景下,适合使用SynchronusQueue。

PriorityQueue & PriorityBlockingQueue

这两种Queue并不是FIFO队列,而是根据元素的优先级进行排序,保证最小元素最先出队,也可以在构造队列时传入Comparator实例,这样PriorityQueue就会按照Comparator实例的要求对元素进行排序。

PriorityQueue是非阻塞队列,也不是线程安全的,PriorityBlockingQueue是阻塞队列,同时也是线程安全的。

Deque的实现类包括LinkedList(前文已经描述过)、ConcurrentLinkedDeque、LinkedBlockingDeque,其实现机制与前文所述的ConcurrentLinkedQueue和LinkedBlockingQueue非常类似,此处不再赘述。

分类 实现 线程安全 排序 特点
List ArrayList × 插入排序 随机访问性能高
LikedList × 插入排序 随机访问性能低,头尾操作性能高,不占用冗余空间
Vector 插入排序 并发性能不高,线程越多性能越差
CopyOnWriteArrayList 插入排序 并发读性能高,占用冗余内存空间
Map HashMap × 无序 读写性能高,接近于O(1)
LinkedHashMap × 插入排序 可按插入顺序遍历,性能与HashMap接近
Hashtable 无序 并发性能不高,线程越多性能越差
ConcurrentHashMap 无序 并发性能比Hashtable高
TreeMap × key升序或降序 有序,读写性能O(logN)
ConcuurentSkipListMap key升序或降序 有序,性能与并发数无关,内存空间占用
Set HashSet × 无序 同HashMa
LinkedHashSet × 插入排序 同LinkedHashMap
TreeSet × 对象升序或降序 同TreeMap
ConcuurentSkipListSet 无序 同ConcuurentSkipListMap
Queue ConcurrentLinkedQueue 插入顺序 非阻塞
LinkedBlockingQueue 插入顺序 阻塞、无界
ArrayBlockingQueue 插入顺序 阻塞、无界
SynchronousQueue - 不存储任何元素,向其中插入元素的线程会阻塞,直到有另一个线程将这个元素取走,反之亦然
PriorityQueue × 对象自然排序或自定义排序 根据元素的优先级进行排序,保证自然序或自定义序最小的对象最先出队
Deque ConcurrentLinkedDeque 插入顺序 非阻塞
LinkedBlockingDeque 插入顺序 阻塞、无界

猜你喜欢

转载自www.cnblogs.com/WanDa92632/p/12506001.html
今日推荐