您的位置:首页 > 其它

HashMap源码分析

2019-04-07 18:21 316 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/qq_26364669/article/details/89044050
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   // 初始容量是16

static final int MAXIMUM_CAPACITY = 1 << 30;         // 最大容量是2^30

static final float DEFAULT_LOAD_FACTOR = 0.75f;       // 负载因子是0.75

static final int TREEIFY_THRESHOLD = 8;                  // 链表转红黑树的阈值

static final int UNTREEIFY_THRESHOLD = 6;	// 红黑树转链表的阈值

static final int MIN_TREEIFY_CAPACITY = 64;       // 转红黑树要求的最小table数组长度

HashMap是Node节点构成的数组+链表/红黑树的结构


关于节点的定义

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 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节点有hash、naxt、key、value四个属性,同时重写了eauqls和hashcode方法。
hashcode
可以看到node节点的hashcode是键与值的hash值相异或而得
equals
首先比较地址是否相同,其次,在是Map.Entry对象类的前提下,如果两者key与value均相同,返回true,否则返回false。

计算hash

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

key的hash计算过程:将key的hashcode与该hashcode的高16位相异或得到hash

根据hash定位索引

int index = (n - 1) & hash;

这里n是table的长度。
若是要将每个节点均匀给到数组,就能合理的避免哈希碰撞,那么使用hash对数组长度进行取模运算是一种不错的方法。接着,x mod 2^n =x & (2^n - 1) ,这里2^n也就是hashmap容量取2的n次方的缘故,所以就可以采用位运算来代替取模运算。
这里将hashcode的高16位与hashcode异或的目的是为了在table的length较小时,让高位也参与运算。

调整hashmap的容量

static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

假设cap=65,n=64=1000000;n>>>1=100000;n|=n>>>1=1100000;同理推得最后n=1111111;再加1就是10000000.
这个函数的目的就是确保hashmap的size始终是2的n次方。 比如你输入18,算出来就是32,需要65,算出来就是128。

getNode

final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //tab是数组,first是hash对应处第一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {      //存在first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))  //first的key对上了
return first;                                               //直接return first
if ((e = first.next) != null) {                 //此时key有next节点
if (first instanceof TreeNode)              //first是树节点
return ((TreeNode<K,V>)first).getTreeNode(hash, key);       //调用树节点的getTreeNode
do {                          //first是链表节点
if (e.hash == hash &&                     //遍历链表找到key对应的节点
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

getNode方法过程总结:
(1)根据hash值得到对应的节点first,若是节点不为空
(2)若是first的hash等于对应hash,且first的key与参数key相等,返回first,否则(3)
(3)否则的话,first为树节点的话,遍历树节点得到对应节点,若first是链表节点,转(4)
(4)若first为链表节点,遍历链表找到对应的节点

putVal

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)   //空数组
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)       //hash对应数组位置处赋值给p,p为空
tab[i] = newNode(hash, key, value, null);    //对应处创建新节点
else {                                          //p不为空
Node<K,V> e; K k;
if (p.hash == hash &&              //对应处有旧值
((k = p.key) == key || (key != null && key.equals(k))))
e = p;                      //旧的节点给e
else if (p instanceof TreeNode)       //对应处是树节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {     //遍历p处的链表节点
if ((e = p.next) == null) {        //p的next给e,若e为null把新节点给e
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st    //大于建树门槛则转为树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&                 //e不为null,且key就是e处的key
((k = e.key) == key || (key != null && key.equals(k))))
break;                      //跳出循环
p = e;                        //将p指向下一个节点
}
}
if (e != null) { // existing mapping for key     //把value给e处的value
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

putval过程总结:
(1)若tab是空数组,调用resize,新长度赋值给n
(2)根据hash得到对应数组节点,将此节点赋值给p,若是为空的话,将新节点直接赋值过来
(3)若p不为空且p的key等于参数k,将p赋值给e(此时p是数组节点)
(4)若p是树节点,调用树方法进行树节点操作
(5)若p是链表节点,遍历p找到对应节点的话,跳出循环,否则,在链表尾部创建新节点插入
(6)对对应节点覆盖原值,若是size超了,进行resize

resize

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;      //pldTab为旧的数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;          //新容量、新门槛都为0
if (oldCap > 0) {                 //如果旧的容量>0
if (oldCap >= MAXIMUM_CAPACITY) {             //旧容量比2^31还大,不能再变了,返回原来的
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&   //新容量是旧容量的2倍且小于max且旧容量比默认容量大
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold     //新门槛是旧的门槛的2倍
}
else if (oldThr > 0) // initial capacity was placed in threshold   //旧容量不大于0且旧门槛大于0
newCap = oldThr;                                             //新容量=旧门槛
else {               // zero initial threshold signifies using defaults 旧容量和旧门槛都是0
newCap = DEFAULT_INITIAL_CAPACITY;         //新容量=默认容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  //新门槛=默认容量*默认负载因子
}
if (newThr == 0) {                   //如果新门槛==0
float ft = (float)newCap * loadFactor;          //ft=新容量*负载因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);      //新门槛=ft或maxvalue
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];              //用新容量创建新的数组
table = newTab;
if (oldTab != null) {                 //如果旧的数组不是null
for (int j = 0; j < oldCap; ++j) {               //遍历旧数组
Node<K,V> e;
if ((e = oldTab[j]) != null) {        //遍历到j,此处节点赋值给e
oldTab[j] = null;                       //j节点置空
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;  //e没next,根据e的hash算出在新数组的位置,将e赋值给该位置节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);   //e是树节点
else { // preserve order                               //e在链表

/*
判断hash&oldcap,如果是0的话,那么hash&(oldcap-1)和hash&(newcap-1)结果是一样的,
比如我们要判断的hash值是1110(14),原始容量若是16,此时hash&(oldcap)==1110&10000==0,
原始index是1110&1111==1110,newcap是32,那么32-1=11111&1110=1110,即新的index和原来额index一样。

若是hash&oldcap不是0,比如我们的hash是10100,oldcap=10000,10100&10000==10000;
那我们的新的index是oldcap+oldindex
oldindex=hash&(oldcap-1)==10100&1111==00100=4
newindex=hash&(newcap-1)==10100&11111==10100=20
其实很容易看出来就只是最前边多了一个1;
*/

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

resize方法总结:
(1)首先如果旧容量>MAXIMUM_CAPACITY,将阈值设为Integer.MAX_VALUE,直接返回老表,否则新容量设为旧容量的2倍,转(2)
(2)如果新容量<MAXIMUM_CAPACITY,且旧容量>DEFAULT_INITIAL_CAPACITY,新的阈值设为旧的阈值的2倍
(3)若旧容量是0,且旧的阈值大于0,新表的容量就是旧表的阈值。这种情况是传了容量的new方法创建的空表,将新表的容量设置为老表的阈值(这种情况发生在新创建的HashMap第一次put时,该HashMap初始化的时候传了初始容量,由于HashMap并没有capacity变量来存放容量值,因此传进来的初始容量是存放在threshold变量上(查看HashMap(int initialCapacity, float loadFactor)方法),因此此时老表的threshold的值就是我们要新创建的HashMap的capacity,所以将新表的容量设置为老表的阈值。
(4)旧容量和旧阈值都是0的话,新容量和新阈值都是默认的。
(5)定义新表,容量就是刚计算出来的新的容量。
(6)将原数组上各个节点重新分布在新表。

treeifyBin

final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //MIN_TREEIFY_CAPACITY是红黑树时要求数组的min长度
resize();                       //此时用resize
else if ((e = tab[index = (n - 1) & hash]) != null) {  //hash对应处不为空
TreeNode<K,V> hd = null, tl = null;
//遍历该索引处的链表
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)   //此时代表是第一次循环
hd = p;
else {
p.prev = tl;        //当前节点prev设施为上一个节点
tl.next = p;        //上个节点的next设为当前节点
}
tl = p;                   //t1赋值为p,作为下次循环的上个节点
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}

链表转红黑树过程:
(1)tab小于64时,resize
(2)hash对应处不为空,遍历该处链表,将链表节点转为红黑树节点,将头节点赋值给pd,
(3)将table该索引位置赋为新转的hd,若该节点不为空,以该节点为根节点构红黑树

treeify

final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;    // root为根节点
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;        //next为x的下一个节点
x.left = x.right = null;           //x左右子节点赋值为空
if (root == null) {                //如果根节点为空,就将x设为根节点
x.parent = null;                //  根节点没有父节点
x.red = false;                  //根节点必须为黑色
root = x;                       //x设为根节点
}
else {                              //存在非空根节点
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)      //x节点hash小于p节点的hash
dir = -1;                   // 代表向p的左边寻找
else if (ph < h)            //x节点hash大于p节点的hash
dir = 1;                //代表向p的右边寻找
//走到这里说明x的hash与p的hash相等,接下来比较k
else if ((kc == null &&         // 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);     //使用自定义的规则来决定dir(寻找方向)
/*
在这个for循环中实际上是从根节点开始找x的位置,p逐级遍历左右子树,直到是空的就找到了
*/
TreeNode<K,V> xp = p;
//向左或者向右找,若是空的说明该位置就是x的位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;    x的父节点是最后一次遍历的p节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//红黑树的插入平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root); // 如果root节点不在table索引位置的头结点, 则将其调整为头结点
}

构建红黑树过程:
(1)如果根节点为空,就将调用该方法的节点置为根节点,否则,根据hash大小去遍历根节点的左右子节点,不断向左向右迭代p节点
(2)若是p节点的next是null,说明找到头了,此时的p节点就是调用该方法的节点的父节点,
(3)进行红黑树的平衡,以及调整头节点位置正确。

关于上边步骤3中的调整头节点位置的方法:

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; //first为index处第一个节点
if (root != first) {                //root不为头节点
Node<K,V> rn;
tab[index] = root;              //root赋值为头节点
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)    //root节点的下一个节点不为空
((TreeNode<K,V>)rn).prev = rp;      //root节点上一个节点赋值为下一个节点的prev
if (rp != null)                 // root节点的上一个节点不为空
rp.next = rn;                //root节点下一个节点赋值为上一个节点的next
//上述操作移除了root节点
if (first != null)          // 如果原头节点不为空, 则将原头节点的prev属性设置为root节点
first.prev = root;
root.next = first;          // 将root节点的next属性设置为原头节点
root.prev = null;         //此时root是头节点,prev为null
}
assert checkInvariants(root);   //检察树是否正常,满足红黑树的规则
}
}

(1)首先确保根节点存在的合理性
(2)如果头节点不是数组index处存在的第一个节点,将root赋值给该第一个节点。
(3)如果root的后节点不为空,将root的前节点赋值给root后节点的prev
(4)如果前节点不为空,将root的后节点赋值给root前节点的next((3)和(4)实际上就是删除root节点的过程)
(5)数组上原来的index处所在的元素是first,若是不为空,就将root节点置为first的前节点
(6)检查树是否正常

移除节点

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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {  //hash对应索引不为空
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&                         //索引处直接对上了
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {            //索引出没直接对上
if (p instanceof TreeNode)                 //返回对应的树节点
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {                                  //此时是链表结构
do {                                //遍历链表找出
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)      //此时是头节点
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}

(1)就是根据hash和key找出对应的节点
(2)节点若是树节点,removeTreeNode
(3)若是普通链表节点,删除链表节点

移除树节点

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;              //空数组直接return
int index = (n - 1) & hash;//索引位置头节点给first和root
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; // next是this.next,this即调用该方法的节点(node)
if (pred == null)               // node节点的prev是null
tab[index] = first = succ;       //将table索引处和first节点赋为succ节点
else                            // node节点的prev不是null
pred.next = succ;             // succ给pred节点的next
if (succ != null)                       // node的next不是null
succ.prev = pred;                 // succ的prev设为pred
if (first == null)              //  first节点是null
return;                     // 直接返回
if (root.parent != null)             // root节点父节点不为空的话,将root节点置为根节点(索引位置头节点不一定是根节点)
root = root.root();
// 通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
// (转链表后就无需再进行下面的红黑树处理)
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map);  // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;   //p为node
if (pl != null && pr != null) {   // node左右子树都不为空
TreeNode<K,V> s = pr, sl;           // s为右子树
while ((sl = s.left) != null) // find successor    // 向左一直查找,直到叶子节点,此时s为叶子节点
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors   交换p和s节点的颜色
TreeNode<K,V> sr = s.right;          // s的右节点
TreeNode<K,V> pp = p.parent;        // p的父节点
if (s == pr) { // p was s's direct parent  //如果p的右节点是叶子结点s
p.parent = s;              // s赋值为p的父节点
s.right = p;              // p赋值给s的右节点
}
else {                      // p的右节点不是叶子节点s
TreeNode<K,V> sp = s.parent;        // sp是s的父节点
if ((p.parent = sp) != null) {      // sp赋值给p的父节点,若sp不是null
if (s == sp.left)               // 如果s为左节点
sp.left = p;                // 将sp左节点赋值p节点
else
sp.right = p;               // s为sp右节点的话,将p给sp的右节点
}
if ((s.right = pr) != null)         // p的右节点给s的右节点且不为空
pr.parent = s;                  //  s给p节点的右节点的父节点
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}

TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

if (replacement == p) {  // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}

(1)数组空的话直接返回
(2)要移除的是调用该方法的树节点node,根据node的hash算出数组中对应index处的first,succ是node的后节点,pred是node的前节点,first赋值给root节点
(3)若pred是null,说明node就在索引处,此时直接把succ赋给索引处(first)
(4)

关于split方法
该方法在前边的resize中用过,作用是将树节点移到新数组。

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;    // b为调用此方法的节点
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {  //从b节点开始遍历
next = (TreeNode<K,V>)e.next;           //next为e下个节点
e.next = null;                          //原来的下个节点置空
if ((e.hash & bit) == 0) {        //为0则扩容后索引不变
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;                           //统计原索引处的节点个数
}
else {                          //此时新的索引为oldcap+原来索引位置
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;                   //统计新的索引处的节点个数
}
}

if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)              //如果个数不足以构造树
tab[index] = loHead.untreeify(map);     //解树
else {                      //否则,如果hihead不是空,说明原来的红黑树被破坏,需要重新构造
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}

(1)b是调用此方法的节点,将b赋值给e
(2)类似于resize方法中来确定lohead/lotail之类的
(3)进行新数组中的分配,lc是原来索引处的节点个数,hc是新的索引处的节点个数
(4)如果个数不足以建树就拆成链表
(5)hiHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置) 已经被改变, 需要重新构建新的红黑树,否则就lohead’完事儿了。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: