List(链表):常用的 ArrayList 和 LinkedList
- ArrayList 底层采用数组方式存储:
ArrayList.class 部分源码
// ArrayList 将所有的元素存储到 elementData 数组中
transient Object[] elementData;
// 实际存储方法
public boolean add(E e) {
// 存储前进行剩余容量判断,防止数组元素溢出
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
// ensureCapacityInternal(...) 会调用 该方法进行扩容
// 对 elementData 数组进行扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// TODO 这个增长因子是重点
// 进行数组扩容,oldCapacity >> 1 结果为 oldCapacity 的一半,也就是说:每次扩容的增长因子为 1.5 ,即原来的 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将原来的数组复制到长度为 newCapacity 的 新数组中,返回给 elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
- LinkedList 底层采用链表形式存储:
LinkedList.class 部分源码
// 底层的存储结构
private static class Node<E> {
// 当前节点
E item;
/// 下一个节点
Node<E> next;
// 上一个节点
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList 类中,有三个比较关键的成员变量:
// 链表首元素
transient Node<E> first;
// 链表末尾元素
transient Node<E> last;
// 链表长度
transient int size = 0;
// 添加元素的方法如下:
public boolean add(E e) {
linkLast(e);
return true;
}
// 实际添加操作由该方法完成
void linkLast(E e) {
// 获取到链表中的最后一个元素
final Node<E> l = last;
// 为当前添加的元素创建一个新的 Node 对象,指定新 Node 对象的上一个节点是 l,
// 注意,没有对 Node 的 next 属性赋值。
final Node<E> newNode = new Node<>(l, e, null);
// 将最后一个元素调整为上一步新建的 Node 对象
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
Set(集合):常用实现类 HashSet
HashSet 是通过持有一个 HashMap 的变量,来进行数据的存储的:
HashSet.class
// 实际进行元素存取操作的对象
private transient HashMap<E,Object> map;
// add(...) 方法会用到它
private static final Object PRESENT = new Object();
// HashSet 的构造方法
public HashSet() {
// map 变量初始化
map = new HashMap<>();
}
// 当向 HashSet 中添加元素时,元素是存储到了 HashMap 的 key 中,value 存储的是一个 Object 对象
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
Map(key-value形式的容器):
常用的实现类有:Hashtable、HashMap、ConcurrentHashMap
- Hashtable:Java 早期的 Key-Value 形式的存储容器,JDK1.0版本便已经存在。来看源码:
// 类声明如下:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
//通过一个 Entry 类型的数组来进行元素的存取操作的
private transient Entry<?,?>[] table;
// 上面那个 Entry 类型,是 Hashtable 的一个私有内部类
private static class Entry<K,V> implements Map.Entry<K,V> {
// 只需要关注这四个属性就可以
// 通过 key.hashCode() 方法生成
final int hash;
// 元素的 key 值
final K key;
// 元素的 value 值
V value;
// 当前元素的下一个元素
Entry<K,V> next;
// other Method ...
}
// 当前的 哈希表中的总的元素个数
private transient int count;
// 当 count 的大小超过 threshold 时,会对 哈希表进行重新整理,可以理解为扩容;
// threshold = 初始化的 table 长度 * loadFactor
private int threshold;
// 哈希表的负载因子
private float loadFactor;
// Hashtable 的默认构造方法
public Hashtable() {
// 这里的这两个参数:
// 11 表示 初始化的 table 数组的长度
// 0.75f 表示 默认的负载因子,即 loadFactor = 0.75f
this(11, 0.75f);
}
// 来看一下 Hashtable 的 put(key,value) 方法
// 通过 synchronized 关键字修饰,意味着方法是同步的
public synchronized V put(K key, V value) {
// 保证 Hashtable 的 value 不能为空
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
int hash = key.hashCode();
// 通过 hash 值来获取到元素在 table 数组中存储的索引
int index = (hash & 0x7FFFFFFF) % tab.length;
// 获取到该索引下的第一个 entry 元素
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 从 entry 开始依次调用其 next 属性,进行 hash值 与 key值 的判断,当新增元素的 hash值 与 key值 在 table 数组中存 // 在,不管 value 是否相等,都用 新增元素的 value 代替已存在的 value
// 这里可能不是很理解,没关系,先往下看,后续会画一个 Hashtable 存储元素的 结构图,结合着图会很容易明白的
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 当 table (哈希表) 中,索引为 hash 的位置为空时,则将当前添加的元素置为索引下的第一个元素
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
...
Entry<?,?> tab[] = table;
// 当 count >= threshold 时,进行 table 扩容
if (count >= threshold) {
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 获取到索引位置,将要存储的对象放到链表头的位置。
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
// 再来看最后一个 rehash() 方法,这个方法是对 table 进行实际扩容的方法
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 新的长度 = 老的长度 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
// 新长度最大为 MAX_ARRAY_SIZE,即 2 的 31次方 - 9
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
...
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 将老的哈希表中的元素依次存入新哈希表
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
}
Hashtable 的数据结构图在这里:
另外,还有很重要的一点需要记住:Hashtable 对外提供的方法都是经过 synchronized 关键字修饰的,也就是说,这些方法都是同步方法。
- HashMap:JDK1.2 以后出现的,功能与 Hashtable 大致相同,只是操作方法不是同步的,并且支持 key、value 为 null
// 来看一下 HashMap 类的 部分关键方法
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 所有的元素都存储在这个 Node 类型的数组对象中
transient Node<K,V>[] table;
public HashMap() {
// 将负载因子设置为 0.75f;也就是说:当哈希表中实际存储元素数量占哈希表容量的 0.75 倍时,会进行 扩容操作;
// 增长因子为 2,即容量增长一倍;关于增长因子先不用管,稍后我们再来看它的具体实现代码
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// HashMap 比较复杂的是它的 put(key,value) 方法,方法有点长,我尽量标注的清楚一些
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 实际进行 put 操作的 方法
// 参数说明:
// hash : 生成方式为 (h = key.hashCode()) ^ (h >>> 16)
// key : 当前的 key 值
// value : 当前的 value 值
// onlyIfAbsent :如果设置为 true,则当哈希表中储存在当前的相同 hash、key 的 key-value 对时,不覆盖已经存在的 value
// evict : 如果为 false,则哈希表处于创建模式
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab = table;
Node<K,V> p;
int n = tab.length;
int i;
// 当 这个 HashMap 对象为刚创建的对象时,table 这个成员变量还未被初始化,
// 则初始化 tab,由于 tab 指向 table,所以,相当于初始化了 table
if (tab == null || n == 0)
n = (tab = resize()).length;
// 这里分为两种情况:
// 1.初始化 tab 后,由于 tab 中还没有元素,所以,存储的元素即为索引下的第一个元素,直接存入
// 2.(n - 1) & hash 计算结果得到的 tab 该索引下还未存入元素,则直接存入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果 tab 索引下的第一个元素恰好与 当前要存入元素的 hash、key 相同,则拿到该元素
// 直接进行 是否覆盖 value 的判断
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果当前的 HashMap 存储结构是 树状结构
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
// 从 索引下第一个元素开始依次获取下一个链表节点,将要插入的元素放入链表末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 当索引下的链表长度超过或者达到 8 时,则将 链表结构 转化为 二叉树结构
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 如果索引下的链表中存在 与当前要插入元素的 hash、key 相同的元素,则跳出 for 循环
// 直接进行 是否覆盖 value 的判断
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果哈希表中存在与当前插入元素同 hash、key 的元素,则 根据 onlyIfAbsent 判断是否进行 value 覆盖
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
...
return oldValue;
}
}
...
// 当 存储的元素长度 达到一定的阈值时,进行扩容;
// threshold = 初始化长度 * 0.75f
if (++size > threshold)
resize();
...
return null;
}
// 扩容方法
// 源码中的 resize() 方法做了很多操作,这里咱们只关注扩容部分
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 当数组长度大于等于 1 << 30 ,即 1073741824 时,则直接将负载阈值设置为 : 2 的 31 次方 -1
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 当数组长度大于等于 16 ,且 新长度设置 老长度的两倍后,新长度依然小于 1 << 30 ,即 1073741824 时,
// 将负载阈值也设置为老负载阈值的两倍
// 多数情况下,会走该 if 内的设置
// 也就是说,扩容时,HashMap 的增长因子为 2,即容量增长为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 这里省略其他处理
...
return newTab;
}
}
HashMap 的数据结构图:与Hashtable 的结构图结合着看会更容易理解一些
-
ConcurrentHashMap : JDK1.5 以后出现。支持高并发的 key-value 容器
上面介绍 Hashtable 的时候,提到:Hashtable 实现线程安全的方式是通过给所有的函数增加 synchronized 关键字来保证线程安全,但是呢,由于 synchronized 添加的位置是在方法级别,作为方法的修饰词来使用的,也就是在使用的过程中,当线程调用 synchronized 修饰的方法时,持有的锁是当前的类对象锁(这里可能还没有概念,先不用管他,只需要记住 synchronized 修饰方法时拿的是类对象锁,后面咱们会单独再来说一下多线程中锁的区别),由此造成该方法虽然是线程安全的,但是也是阻塞的,并发性并不是很高(并发性不高:多个线程同时访问这个方法时,只能有一个线程访问成功,其他线程阻塞等待,知道访问成功的那个线程运行完该方法,这时阻塞等待的线程们再去争抢类对象锁,抢到的运行方法,其他阻塞等待,依次循环,知道所有线程运行完该方法)。而并发性不高,则就意味着代码的执行效率不高,所以, 就有了 ConcurrentHashMap 这个支持高并发的 key-value 容器的出现。
而 ConcurrentHashMap 支持高并发的方式也很简单,就是通过桶锁的方式来实现的,桶锁这个概念是不是有点陌生呢,不用担心,它其实很简单,下面,我们先从 ConcurrentHashMap 的源码看起:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
// 实际存储元素的是一个 Node 数组;留意这里的 volatile 关键字
// 看吧,ConcurrentHashMap、HashMap、Hashtable 的底层都是通过一个数组类型的对象来进行元素的存储的
transient volatile Node<K,V>[] table;
// 默认的负载因子,ConcurrentHashMap、HashMap、Hashtable 他们的负载因子都是 0.75f
private static final float LOAD_FACTOR = 0.75f;
// table 的初始化长度,默认为 16
private transient volatile int sizeCtl;
// 下面来看它的 put 方法
public V put(K key, V value) {
return putVal(key, value, false);
}
// 源码这个方法有点长,在这里我省略了部分不需要关心的内容,咱们只看关键的部分
// 参数说明:
// key : key 值
// value : value 值
// onlyIfAbsent : 如果为 false,则表示当当前存入元素的 hash、key 在 哈希表中已经存在时,则覆盖老的 value 值
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ConcurrentHashMap 不允许 key 或者 value 任一为空
// 这里我们回顾一下:Hashtable 仅是不允许 value 为空
// HashMap 则允许 key value 为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 哈希表为空,则证明是第一次向该 ConcurrentHashMap 中添加元素,进行哈希表初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 当 (n - 1) & hash 这个索引下还是空的时,则将当前插入的元素作为链表的头元素放到索引下;然后直接返回成功
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break;
}
// 这个 else if 不用关注,仅留意 fh = f.hash 即可
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用 synchronized 代码块,要进入 synchronized 代码块的线程需要持有 f 的锁才可以
// 而 f 则是 table 数组下,某一索引下的头元素,或者说是索引下链表的表头节点
synchronized (f) {
// 双重所判断机制,确定 ConcurrentHashMap 的哈希表结构未被改变,i 索引下的链表头节点依然是 f
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
// 开始通过 next 属性,从链表头开始依次获取下一个 节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果当前存入节点的 hash、key 与 链表中某节点的 hash、key 相同
// 则根据 onlyIfAbsent 判断 是否覆盖老的 value 值
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 如果到了链表末尾,链表中不存在 hash、key 与但前插入元素的 hash、key 相同的元素,
// 则将新增的元素插入的链表末尾
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,value, null);
break;
}
}
}
// 如果索引下的存储结构是二叉树结构
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 当链表深度超过或者达到 8 时,根据 table 的长度决定是否 进行 链表结构 转化为 二叉树结构
// 这个 结构转换的代码可以参考 HashMap 的 链表 转 二叉树 结构的那个结构图的介绍,基本大同小异
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
...
return null;
}
// 这个方法我们只关心容量扩容的部分
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// 如果 tab 长度 小于 64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 该方法中会直接调用 当前的 table 变量,n << 1 为的 table 的长度,而 n << 1 的结果为 table.leng * 2
// 也就是说,ConcurrentHashMap 的增长因子为 2
// 至此,ConcurrentHashMap、HashMap、Hashtable 的增长因子我们都已经知道,均为 2 ,即容量增长为原来的 2 倍
// 同时,还需注意,他们的负载因子均为 0.75f ,即当实际存入元素数量达到 table 的 0.75 倍,则进行扩容
tryPresize(n << 1);
else if ...
}
}
}
下面这个图是 ConcurrentHashMap 的存储结构图:与 HashMap 大同小异,只是多了一个桶锁的概念