- hash值的计算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1.1 为什么要hash = h ^ (h >>> 16)?
如果不做此操作,计算槽号:
(2^n - 1) ^ hashcode:
导致只有低n位参与运算。如果高16位改变,但是计算出来的hash没有任何改变。
所以,如果多个hashcode的低n位相同,而高位不同,得到的槽号相同,这使得hash表的散列不均匀,所以,为了让高16位也参与运算
h >>> 16:算术右移16位,得到的是原来的高16位变为低16位,高16位补0。
由此高16位可以和低16位运算。
1. 2 为什么是异或操作?
为了保留高16位和低十六位的特征(为1的位)。当高16位或者低16位改变时,hash值可以跟着它改变,比如,高位从 1 变成0,低位为0时,异或结果由1变0;低位为1时,结果由0变1,使hash表更灵敏。
异或(^)是为 0,1的情况得到1,& 为1,1时得到1,它对高位或者低位的变化反应不灵敏,低位位0时,高位由1变0,hash对应的值仍为0,没有发生变化;只有低位为1时,高位发生变化,hash也跟着变,但是这不是我们想看到的。我们想要是只要高位或者低位发生变化,hash值就会改变。
那或操作呢?和 & 是一个情况啊。或操作是为0,0时得到0,比如,低位为1,高位由0变1,得到的结果永远是1,反应也不灵敏。所以我们采用异或操作。这样高16位和低16位发生改变时,我们可以得出不同的hash值,这样充分发挥了每个hashcode的高16位和低16位的特性,使得根据其计算出来的槽号尽可能不同,使它们均匀地分在hash表中,增大了hash表的散列程度。
1.3 槽位数为什么是 2^n?
为2^n时,槽位数-1 得到的低n位全部为1,进行与运算时,保留下来的是hash的低n位,如果不是2^n次方,比如为2^n+1,槽位数-1,低n位全为0,进行与运算得到的结果为低n+1位与的结果,该位要么为0要么为1,所以最后得到的槽位为0或2^n,所有hash运算的结果都得到这两个数字,所有结果都保留在这两个槽位中。
1.4 为什么是 (2^n-1) & hash?
相当于对数组长度取余。
2. HashMap方法解析
2.1 get()方法
HashMap的key可以为空对象吗?
可以。一切对象都有hashcode,null也是对象,为空对象时,计算出的hash为0。
得到槽位 (2^n-1) & hash: 为0。
而hashmap在put时,
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
为其new一个Node对象,hash为0,key,value为null,此时table[0]存了一个新的节点。
hashmap的value也可以是null。
那key为null,containsKey(key)得到什么呢?
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
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;
}
containsKey(key)调用getNode()方法,getNode方法看前七行就好,从table[0]获得Node对象,此对象不为空,因为已经为它在堆中分配内存和初始化,只是初始化的key和value都为null,所以getNode返回一个不为空的对象,所以containsKey(key)返回true。
为什么重写hashCode()方法?
不重写,Object的hashcode默认根据地址值生成,只要new两个对象,那么得到的哈希值一定不同,这不是我们想要看到的。
hashmap保留元素的前提是key对象的属性值不能相同,而不是地址。如果不重写equals方法,默认使用Object对象的equals方法,比较的是地址。
public boolean equals(Object obj) {
return (this == obj);
}
首先要根据hash计算槽位,其次找到位置,如果该位置由元素,要判断hash值是否相同,由于会产生哈希碰撞,还要判断key的值是否相同,key值如果为基本数据类型,用==判断,为引用数据类型,用equals判断,所以key对象的equals方法要进行重写,比较属性值是否相同。
执行流程:
2.2 HashMap各阈值
容量:初始数组容量为16,阈值为0.75,当负载 > 16*0.75时,要将数组扩容为原来的2倍,即32
扩容也有长度限制:
static final int MAXIMUM_CAPACITY = 1 << 30;
当超过此值,不能扩容。
int有32位,最高位代表符号位,所以容量最大值为 2^30
树级阈值:
当链表长度大于8 并且 hashMap的元素值至少是64时,将其自动转化为红黑树,在此之前都将是数组+链表形式。
链表阈值:
当桶中元素 < 6,自动转化成链表
HashMap的每次操作都要new一个新数组(String同理)
2.3 put() 方法
源码:
//添加元素三种情况:
// 1.数组位置是null
// 2.数组位置不是null,key不重复,挂在链表或者红黑树上
// key重复,覆盖
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义局部变量,每次不用专门去堆中寻找table
Node<K,V>[] tab;
//临时变量
Node<K,V> p;
// 当前数组长度
int n;
//索引
int i;
tab = table; //第一次赋值过来的为null
n = tab.length;
if (tab == null || n == 0){
// 1.第一次添加数据,创建一个长16,加载因子为0.75的数组
// 2.不是第一次,看数组是否达到了扩容条件;是扩容两倍,将数据移交到新的哈希表,否不处理
tab = resize();
//将当前数组长度赋值给n
n = tab.length;
}
//计算出当前键值对对象在数组中应存在的位置
i = (n - 1) & hash;
p = tab[i];
//数组位置是null
if (p == null)
//创建键值对对象,直接放到数组 拿key计算哈希值
tab[i] = newNode(hash, key, value, null);
//数组位置不为null
else {
Node<K,V> e; K k;
//首先判断数组位置的元素与待添加元素hash是否同 键值重复
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) {
//获取数组元素的下一个节点
e = p.next;
if (e == null) {
p.next = newNode(hash, key, value, null);
//添加完之后看是否需要调整
//判断当前链表长度是否超过8
//treeifyBin判断数组长度是否大于64,同时满足将链表转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//key重复,不添加到链表了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //下次循环
}
}
// e为null,无需覆盖元素
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//等号右边为要添加的值 覆盖:value进行覆盖,原先的键值对还在
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// thredshold:n*0.75 扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
// 当时没有覆盖任何元素
return null;
}
流程图:
2.4 remove()方法
源码:
// 删除指定key的键值对,返回被删除的键值对的值
// 返回为空,key可能不存在,或key为null
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// value是否删除,取决于 matchValue,为true时,当key和value都相等时才会删除,false则不管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;
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))))
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)))) {
//找到了 break循环
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// node为找到的与key匹配的目标节点
// !matchValue : 当matchValue为false时,代表不需要关心值,判断短路,不走后面
// true, 要比较值了,继续走后面的判断
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);
//找到node了,node=p时,代表当前槽的节点是node(链表或者树的第一个节点)
else if (node == p)
tab[index] = node.next;
// 否则,p为目标节点的前一个节点,删除node节点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
//返回被删除的节点
return node;
}
}
// 没有找到节点,返回null
return null;
}
执行流程: