目录
2.3.1 HashMap(int initialCapacity, float loadFactor)
2.3.2 HashMap(int initialCapacity)
本文基于JDK1.8 对HashMap的底层数据结构、方法实现原理进行分析。
1.类的底层数据结构
Java HashMap底层采用哈希表结构(数组+链表或红黑树)实现,集合了数组和链表的优点:
a.数组优点:可以通过数组下标快速获取元素,效率极高
b.链表有点:插入或删除不需要移动元素,只需修改节点的引用,效率极高
hashMap底层数据结构图:
2.类结构
2.1 元素节点Node<K,V>
HashMap底层使用数组存储数据,数组的类型为Node<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Node节点包含4个元素:
hash: 根据key通过hash()方法计算的hash值
key: 键值对的键
value:键值对的值
next: 表示当前节点的下一个节点
2.2 红黑树节点TreeNode<K,V>
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
......
}
红黑树的节点使用TreeNode表示,当链表中的元素超过8个(TREEIFY_THRESHOLD)并且数组长度大于64(MIN_TREEIFY_CAPACITY)时,链表会转换为红黑树。
2.3 构造函数
HashMap包含的几个重要变量及常量:
//数组默认初始长度 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//数组最大长度,2的30次幂,即1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转红黑树的长度阈值
static final int TREEIFY_THRESHOLD = 8;
//红黑树转链表的长度阈值
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化为红黑树时,数组容量必须大于等于64
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap使用数组存放数据,数组元素类型为Node<K,V>
transient Node<K,V>[] table;
//HashMap中键值对的个数
transient int size;
// 用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),直接抛出ConcurrentModificationException异常
transient int modCount;
//数组扩容阈值,计算方式为 数组容量*加载因子。
int threshold;
//加载因子
final float loadFactor;
2.3.1 HashMap(int initialCapacity, float loadFactor)
HashMap(int initialCapacity, float loadFactor)指定了数组初始容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
//校验指定数组长度
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
//指定数组长度大于数组默认最大值2^30,initialCapacity赋值为MAXIMUM_CAPACITY
initialCapacity = MAXIMUM_CAPACITY;
//校验加载因子,当加载因子<=0或者不是float类型时,抛出IllegalArgumentException异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//获取>=指定容量的最小的2的n次幂
this.threshold = tableSizeFor(initialCapacity);
}
使用该构造函数时,数组扩容阈值threshold 为指定容量的最小的2的n次幂,只有调用put方法时,
threshold才会被赋值为:数组容量*加载因子。
2.3.2 HashMap(int initialCapacity)
HashMap(int initialCapacity)指定了数组初始容量,使用默认加载因子
public HashMap(int initialCapacity) {
//指定数组长度,使用默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
2.3.3 HashMap()
HashMap()无参构造函数
public HashMap() {
//设置加载因子为默认加载因子 0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2.4 put(K key, V value)
put(K key, V value) 指定键值对,插入元素
public V put(K key, V value) {
//根据key调用hash()方法结算hash值
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
//计算key的哈希值,当key为null,返回0,否则通过(h = key.hashCode()) ^ (h >>> 16)公式计算哈希值。
//该公式通过hashCode的高16位异或低16位,得到哈希值,主要从性能、哈希碰撞角度考虑,不会造成因为高位没有参与下标计算从而引起的碰撞
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//如果数组为null或者长度为0,则进行初始化操作
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根据key的哈希值计算出key在数组中存放的下标位置,公式为(n-1)&hash
if ((p = tab[i = (n - 1) & hash]) == null)
//如果目标下标位置没有元素,创建新的Node节点,并插入
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果目标位置已存在元素,并且已有元素的哈希值、key值和新插入元素的哈希值、key值匹配,将已有节点赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果目标位置已存在元素p,并且p节点为红黑树,则插入红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//否则目标位置已存在元素p,并且p节点为链表结构,遍历链表,尾部插入
for (int binCount = 0; ; ++binCount) {
//如果当前节点p的next节点为null
if ((e = p.next) == null) {
//创建新的node节点,赋值给当前节点p的next节点
p.next = newNode(hash, key, value, null);
//如果链表长度大于等于TREEIFY_THRESHOLD,则考虑转为红黑树。binCount从0开始,所以TREEIFY_THRESHOLD-1
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果链表中节点的哈希值、key值和新插入元素的哈希值、key值是否匹配,将已有节点赋值给e
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e不为null,则数组中存在插入的key
if (e != null) { // existing mapping for key
//获取已存在节点的旧值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//将节点e的值替换为新值
e.value = value;
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//修改次数递增
++modCount;
//当键值对个数大于等于扩容阈值的时候,进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put操作过程如下:
a.判断数组是否为null或者长度为0,是的话初始化数组(所以在创建HashMap时,并不会初始化数组)
b.通过(n - 1) & hash计算key在数组中存放的下标位置
c.如果目标下标位置没有元素,创建新的Node节点,并插入
d.如果目标下标位置不为空,分为下面三种情况:
d.1 已有元素的哈希值、key值和新插入元素的哈希值、key值匹配,覆盖旧值
d.2 该节点为红黑树,则插入红黑树中
d.3 该节点为链表,遍历链表,尾部插入
e. 如果链表长度大于等于TREEIFY_THRESHOLD=8,则考虑转为红黑树。binCount从0开始,所以是binCount>=TREEIFY_THRESHOLD-1
f.判断HashMap元素个数是否大于等于threadhold(扩容阈值),是的话,进行扩容
2.5 get(Object key)
get(Object key) 获取指定key的元素
public V get(Object key) {
Node<K,V> e;
//获取指定key的元素,如果元素为null,返回null,否则返回元素的值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组是否为空,并且长度长度是否大于0,目标索引位置下元素是否为空,是的话就直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果目标索引位置元素就是要找的元素,直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果目标索引位置元素的下一个节点不为空
if ((e = first.next) != null) {
//如果类型是红黑树,就从红黑树中查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//否则就是链表,遍历链表查找目标元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
2.6 resize()
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;
//扩容前数组长度大于0,当前数组有数据
if (oldCap > 0) {
//如果扩容前的数组长度大于MAXIMUM_CAPACITY,就不扩容了,返回扩容前数组
if (oldCap >= MAXIMUM_CAPACITY) {
//设置数组的阈值为Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩大新数组的容量为老数组容量的两倍,但必须小于MAXIMUM_CAPACITY,并且老容量大于等于 默认初始长度16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩大新数组的阈值为老数组阈值的两倍
newThr = oldThr << 1; // double threshold
}
//当前数组没有数据,新数组的容量赋值为老数组的阈值,因为new HashMap(int initialCapacity, float loadFactor)时,initialCapacity赋值给了threadhold
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//当使用无参构造方法 new HashMap()时,新数组的容量使用默认值 DEFAULT_INITIAL_CAPACITY 16,新数组的阈值为默认容量*默认加载因子
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//当新数组的阈值为0时,计算新数组的阈值,公式为新数组的容量*加载因子,最大值为Integer.MAX_VALUE
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新数组的阈值赋值给threshold属性
threshold = newThr;
//创建一个新的数组,数组类型为Node,长度为新数组的容量值 newCap
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新数组赋值给数组table属性
table = newTab;
if (oldTab != null) {
//如果旧数组不为null,说明旧数组中有元素,遍历旧数组,将旧数组中的元素赋值到新数组
for (int j = 0; j < oldCap; ++j) {
//声明一个节点e,用来接收遍历的节点
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//旧数组当前下标的元素不为null,将旧数组当前下标的元素赋值为null,便于GC
oldTab[j] = null;
if (e.next == null)
//如果当前元素的下一个节点为null,说明当前元素没有后继节点
//直接将当前元素插入到新数组的指定位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果当前元素为树形结构,
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//否则当前元素为链表结构,遍历链表 ,将链表中的元素使用尾插法插入新的数组
//定义两个链表,低位链表:loHead: 低位的头节点 loTail:低位的尾节点
//高位链表:hiHead: 高位的头节点 hiTail:高位的尾节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//当前节点的hash&oldCap==0,在新数组索引位置不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
//低位尾节点为null,将当前节点赋值给低位头节点loHead,插入第一个低位元素
loHead = e;
else
//低位尾节点不为null,将当前节点赋值给loTail
loTail.next = e;
loTail = e;
}
else {
//否则在新数组索引位置改变,索引位置为原索引+oldCap
if (hiTail == null)
//高位尾节点为null,将当前节点赋值给高位头节点hiHead,插入第一个高位元素
hiHead = e;
else
//高位尾节点不为null,将当前节点赋值给loTail
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;//将低位尾节点的下一个节点赋值为null
newTab[j] = loHead;//将低位链表的头节点插入新数组中,插入下标为原来元素所在的位置
}
if (hiTail != null) {
hiTail.next = null;//将高位尾节点的下一个节点赋值为null
newTab[j + oldCap] = hiHead;//将高位链表的头节点插入新数组中,插入下标为原来元素所在的位置+oldCap(旧数组的容量)
}
}
}
}
}
return newTab;
}
扩容时,当节点为链表结构,旧数组中的元素在新数组中存放位置通过e.hash&oldCap
结果是否为0来计算,主要有两种情况:
情况一:
扩容前oldCap=16,hash=1,则在旧数组中的存放位置 (n-1)&hash=15&1=1
扩容后newCap=32,hash=1, 则在新数组中存放的位置(n-1)&hash=31&1=1
存储位置没有不变,新的下标位置和原下标位置相同,hash&oldCap=1&16=0
情况二:
扩容前oldCap=16,hash=17,则在旧数组中存放的位置 (n-1)&hash=15&17=1
扩容后newCap=32,hash=17,则在新数组中存放的位置 (n-1)&hash=31&17=17
存储位置改变,新位置为原下标位置+oldCap=1+16=17,hash&oldCap=17&16=16
所以我们用公式 e.hash&oldCap是否为0来计算元素扩容后的新下标位置
2.7 remove(Object key)
remove(Object key) 删除指定元素
public V remove(Object key) {
Node<K,V> e;
//被删除直接key的节点是否为null,是null,直接返回,否则返回删除节点的value值
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果数组不为null,长度大于0,并且数组中指定key所在下标的元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//当前节点的hash值、key值和要删除的指定key的hash值、key值匹配,把当前节点赋值给临时节点node
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
//当前节点是树形结构,调用树形结构的方法getTreeNode()方法获取节点,并赋值给临时节点node
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//否则节点为链表结构,遍历链表,找到匹配节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
//找到链表中匹配的节点,并赋值给临时节点node
node = e;
break;
}
//如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
//如果节点为树形结构,调用树形结构的方法removeTreeNode删除节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
//如果要删除的节点为数组下标节点,也就是头结点,直接让下一个作为头
//要删除的节点为头节点,分为两种情况 1.节点为普通节点 2.节点为链表结构,头节点就是要删除的节点
tab[index] = node.next;
else
//删除的节点在链表中,把要上一个结点next节点 指向 删除节点的next节点
p.next = node.next;
//修改计数器
++modCount;
//长度减一
--size;
afterNodeRemoval(node);
//返回删除的节点
return node;
}
}
return null;
}
remove()方法操作过程如下:
a.判断数组不为空、长度大于0,并且通过(n - 1) & hash计算key在数组中存放的下标位置元素不为空
b.当前节点的hash值、key值和要删除的指定key的hash值、key值匹配,赋值给临时节点node
c.当前节点的next节点不为空,分为下面两种情况:
c.1 当前节点是树形结构,赋值给临时节点node
c.2 该节点为链表,遍历链表,赋值给临时节点node
d.临时节点node为树形结构,调用树形结构删除方法
e.临时节点node为非树形结构,分为下面两种情况:
e.1 如果要删除的节点为数组下标节点,也就是头结点,直接让数组当前位置指向临时节点node的next节点
e.2 删除的节点在链表中,把要上一个结点next节点 指向 删除节点的next节点
总结:
HashMap代码写很巧妙,值得深读。
后续再单独写一篇解析HashMap关于红黑树操作、链表转红黑树、以及红黑树转链表的源码解析。