虽然之类的文章已经很多了,但是自己还是有必要总结一下,综合很久之前自己云笔记上的内容和一些其他博客上的内容,希望最后这些全部写完了能够形成一条链,在写的过程中也是一种复习和提升吧。希望对自己和需要的人能有一点帮助。这一篇就整体讲一下Java集合框架的内容,以下是这篇博客的大纲。
一.集合框架的概念
二.顶级接口和部分其他接口的介绍
三.几个重要的子类简介
四.应用场景(比较)
五.总结
一.集合框架的概念(Collections Framework)
集合代表了一组对象(和数组一样,但数组长度不能变,而集合能)。Java中的集合框架定义了一套规范,用来表示、操作集合,是具体操作与实现细节解耦。其实说白了,可以吧一个集合看成一个微型数据库,主要是增删改查这集中操作,我们在学习使用一个具体的集合类的时候只需要把这几个操作搞清楚就可以了。
二.顶级接口和部分其他接口的介绍
在集合框架的类里面,最顶层的是两个接口:
---Collection 表示一组纯数据
---Map 表示一组key-value对
一般继承自Collection或者Map的集合类,会提供两个标准的构造函数:①无参构造函数,创建一个空的集合类②有一个类型和基类(Collection或Map)相同的构造函数,创建一个与给定参数具有相同元素的新集合类。
Collection基类及子类树形结构图如下
如上图所示,Collection类主要有三个接口:
①Set表示不允许有重复元素的集合(A collection that contains no duplicate elements),Set接口与List接口的重要区别就在于它是不支持重复元素的,比如最多只能包含一个null值。
②List表示允许有重复元素的集合(An ordered collection (also known as a sequence)),使用List接口能够精确控制每个元素被插入的位置,并且可以通过元素在列表中的索引来访问它。允许有重复的元素,并且允许一个或多个null值
③Queue JDK1.5新增,与上面两个集合类的主要区别在于Queue主要用于存储数据,而不是处理数据。(A collection designed for holding elements prior to processing)一般队列实现允许我们高效的在队尾添加元素,在队列头部删除元素(FIFO)。Queue<E>接口还有一个名为Deque的子接口,允许我们在队头或者队尾添加/删除元素,实现Deque<E>的接口的集合类即为双端队列的一种实现(比如LinkedList就实现了Deque接口)
Map基类及子类树形结构图如下
Map并不是一个真正意义上的集合(are not true collections),但是这个借口提供了三种"集合视角"(collection views),是的可以像操作集合一样操作他们,具体如下:
①把map的内容看做key的集合(map's contents to be viewed as a set of keys)
②把map的内容看做value的集合(map's contents to be viewed as a collection of values)
③把map的内容看作key-value映射的集合(map's contents to be viewed as a set of key-value mappings)
三.几个重要的子类简介
这里的话简单的介绍这些接口和一些重要常用的实现类,具体的话肯定会在后面写一些关于这些子类的详细分析,到时候再把链接贴到这里。
1)List类集合
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使用数组拷贝来实现指定位置的插入和删除。
插入:
删除:
②LinkedList
表示了一个双向链表,和ArrayList一样是非线程安全的。基于链表来实现集合的功能,实现了静态类Node,集合中的每一个元素都由一个Node保存,每一个Node都拥有到自己前一个和后一个Node的引用
LinkedList追加元素的过程示例:
③Vector
Vector和ArrayList很像,都是基于数组实现的集合,它和ArrayList的主要区别在于:
Vector是线程安全的,而ArrayList不是
由于Vector中的方法基本都是synchronized的,其性能低于ArrayList
Vector可以定义数组长度扩容的因子,ArrayList不能
④CopyOnWriteArrayList
与 Vector一样,CopyOnWriteArrayList也可以认为是ArrayList的线程安全版,不同之处在于 CopyOnWriteArrayList在写操作时会先复制出一个副本,在新副本上执行写操作,然后再修改引用。这种机制让 CopyOnWriteArrayList可以对读操作不加锁,这就使CopyOnWriteArrayList的读效率远高于Vector。 CopyOnWriteArrayList的理念比较类似读写分离,适合读多写少的多线程场景。但要注意,CopyOnWriteArrayList只能保证数据的最终一致性,并不能保证数据的实时一致性,如果一个写操作正在进行中且并未完成,此时的读操作无法保证能读到这个写操作的结果。
2)Map类集合
Map将key和value封装至一个叫做Entry的对象中,Map中存储的元素实际是Entry。只有在keySet()和values()方法被调用时,Map
才会将keySet和values对象实例化。每一个Map根据其自身特点,都有不同的Entry实现,以对应Map的内部类形式出现。几个实现类介绍如下:
①HashMap
底层是用数组+链表+红黑树来实现的,HashMap将Entry对象存储在一个数组中,并通过哈希表对Entry的快速访问。
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更高效的并发性能。
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的。
④LinkedHashMap
LinkedHashMap与HashMap非常类似,唯一的不同在于前者的Entry在HashMap.Entry的基础上增加了到前一个插入和后一个插入
的Entry的引用,以实现能够按Entry的插入顺序进行遍历。
⑤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的排序和快速查找。关于红黑树的具体介绍,可以点击这里参考这篇文章
TreeMap的Entry是有序的,所以提供了一系列方便的功能,比如获取以升序或降序排列的KeySet(EntrySet)、获取在
指定key(Entry)之前/之后的key(Entry)等等。适合需要对key进行有序操作的场景。
⑥ConcurrentSkipListMap
ConcurrentSkipListMap同样能够提供有序的Entry排列,但其实现原理与TreeMap不同,是基于跳表(SkipList)的:
如上图所示,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一样,不能保证数据的绝对一致性,在某些情况下有可能无法读到正在被插入的数据。
3)Set实现类
Set 接口继承Collection,用于存储不含重复元素的集合。几乎所有的Set实现都是基于同类型Map的,简单地说,Set是简化版的Map
。每一个Set内都有一个同类型的Map实例(CopyOnWriteArraySet除外,它内置的是CopyOnWriteArrayList实例),Set把元素
作为key存储在自己的Map实例中,value则是一个空的Object。Set的常用实现也包括 HashSet、TreeSet、ConcurrentSkipListSet等
,原理和对应的Map实现完全一致,此处不再赘述。
4)Queue/Deque实现类
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<E> {
volatile E item;
volatile Node<E> next;
}
由于Node类的成员都是volatile的,所以ConcurrentLinkedQueue自然是线程安全的。能够保证入队和出队操作的原子性和一致性,但在遍历和size()操作时只能保证数据的弱一致性。
②LinkedBlockingQueue
与ConcurrentLinkedQueue不同,LinkedBlocklingQueue是一种无界的阻塞队列。所谓阻塞队列,就是在入队时如果队列已满,
线程会被阻塞,直到队列有空间供入队再返回;同时在出队时,如果队列已空,线程也会被阻塞,直到队列中有元素供出队时再
返回。LinkedBlocklingQueue同样基于链表实现,其出队和入队操作都会使用ReentrantLock进行加锁。所以本身是线程安全的,
但同样的,只能保证入队和出队操作的原子性和一致性,在遍历时只能保证数据的弱一致性。
③ArrayBlockingQueue
ArrayBlockingQueue是一种有界的阻塞队列,基于数组实现。其同步阻塞机制的实现与LinkedBlocklingQueue基本一致,区别仅在于
前者的生产和消费使用同一个锁,后者的生产和消费使用分离的两个锁。
④SynchronousQueue
SynchronousQueue算是JDK实现的队列中比较奇葩的一个,它不能保存任何元素,size永远是0,peek()永远返回null。向其中插入
元素的线程会阻塞,直到有另一个线程将这个元素取走,反之从其中取元素的线程也会阻塞,直到有另一个线程插入元素。这种实现
机制非常适合传递性的场景。也就是说如果生产者线程需要及时确认到自己生产的任务已经被消费者线程取走后才能执行后续逻辑的
场景下,适合使用SynchronousQueue。
⑤PriorityQueue和PriorityBlockingQueue
这两种Queue并不是FIFO队列,而是根据元素的优先级进行排序,保证最小的元素最先出队,也可以在构造队列时传入Comparator
实例,这样PriorityQueue就会按照Comparator实例的要求对元素进行排序。
PriorityQueue是非阻塞队列,也不是线程安全的,PriorityBlockingQueue是阻塞队列,同时也是线程安全的。
四.应用场景(比较)
①ArrayList和LinkedList
ArrayList的随机访问效率更高,基于数组实现的ArrayList可直接定位到目标元素,而LinkedList需要从头结点或者尾结点向前或者向后遍历若干次才能找到目标元素。
LinkedList在头尾结点执行插入或删除操作的效率比ArrayList高
由于ArrayList每次扩容的容量是当前的1.5倍,所以LinkedList所占用的内存空间要更小一点。
两者遍历的效率接近,但需要注意,遍历LinkedList的时候要用iterator方式,不要用get(int)的方式否则效率很低。
②Vector和CopyOnWriteArrayList
二者均是线程安全的、基于数组实现的List
Vector是【绝对】线程安全的,CopyOnWriteArrayList只能保证读线程会读到【已完成】的写结果,但无法像Vector一样实现
读操作的【等待写操作完成后再读最新值】的能力
CopyOnWriteArrayList读性能远高于Vector,并发线程越多优势越明显
CopyOnWriteArrayList占用更多的内存空间
③HashMap,Hashtable和ConcurrentHashMap
三者在数据存储层面的机制原理基本一致
HashMap不是线程安全的,多线程环境下除了不能保证数据一致性之外,还有可能在rehash阶段引发Entry链表成环,导致
死循环
Hashtable是线程安全的,能保证绝对的数据一致性,但性能是问题,并发线程越多,性能越差
ConcurrentHashMap 也是线程安全的,使用分离锁和volatile等方法极大地提升了读写性能,同时也能保证在绝大部分情况下
的数据一致性。但其不能保证绝对的数据一致性, 在一个线程向Map中加入Entry的操作没有完全完成之前,其他线程有可能
读不到新加入的Entry
④TreeMap和ConcurrentSkipListMap
二者都能够提供有序的Entry集合
二者的性能相近,查找时间复杂度都是O(logN)
ConcurrentSkipListMap会占用更多的内存空间
ConcurrentSkipListMap是线程安全的,TreeMap不是
⑤ConcurrentLinkedQueue、LinkedBlockingQueue和ArrayBlockingQueue
ConcurrentLinkedQueue是非阻塞队列,其他两者为阻塞队列
三者都是线程安全的
LinkedBlocklingQueue是无界的,适合实现不限长度的队列, ArrayBlockingQueue适合实现定长的队列
五.总结