继承类:AbstractMap
继承接口:Map,Cloneable,Serializable
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//onlyIfAbsent,if true,则表示只要当前节点的value != null则不会改变当前节点的value值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//判断当前的table是否为空,若为空或长度为0,进行扩容初始化,并将其扩容后的长度赋值给n
if ((tab = table) == null || (n = tab.length) == 0)
//resize()中对table进行了初始化操作
n = (tab = resize()).length;
//判断数组当前索引没有元素,则直接添加新节点
if ((p = tab[i = (n - 1) & hash]) == null)//当n为2的次方数时(扩容时必须已经考虑到这个条件),满足(n - 1) & hash 等价于 hash % n,这里采用&运算性能更高
tab[i] = newNode(hash, key, value, null);
else {
//若当索引处有节点元素
Node e; K k;
//1.如果key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//直接覆盖
e = p;
//2.key不相等的情况,判断当前节点是否是TreeNode,即是红黑树的情况
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
//3.key不相等,在链表后添加新节点,即链表的情况
else {
//遍历链表结构
for (int binCount = 0; ; ++binCount) {
//链表结构中没有对应的key
if ((e = p.next) == null) {
//添加新的节点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //这里binCount是从0开始计数,所以需要与TREEIFY_THRESHOLD - 1 = 8 - 1 = 7进行比较
//将当前链表结构转换成对应的树结构
treeifyBin(tab, hash);
break;
}
//树中有对应的key的节点,跳出遍历
if (e.hash == hash &&//判断是否产生hash碰撞,产生碰撞后比较其key值是否真正相等
((k = e.key) == key || (key != null && key.equals(k))))
//相等则跳出循环,当前e即为对应key的节点
break;
//即 p = p.next
p = e;
}
}
//若e不为空则说明存在当前key的节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//将新value赋值给节点e
e.value = value;
afterNodeAccess(e);
//返回当前key对应的旧的value
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
之前看了很多博客上的流程图,我只想说 like a piece of shit,大部分人都是进行copy,根本没完善细节也没有结合源码进行理解和思考。建议可以对照该图和其他博客的图示进行对比,找出不同的地方,以及理解为什么不一样。
public V get(Object key) {
Node e;
//找到对应key的节点并返回value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
//当table的长度大于0且根据hash的摸计算得到对应的索引元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//线对第一个节点进行判断,当一个节点的hash相等,key内存地址相等,key对象的内容相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//满足条件后返回对应的节点
return first;
//第一个节点的条件不满足的情况下,进行遍历链表结构或树结构查找是否有对应的值
if ((e = first.next) != null) {
//判断当前节点是否为TreeNode,当Map中的某个链表存储的长度过长会自动转换为树结构
if (first instanceof TreeNode)
//遍历树结构取出对应的值
return ((TreeNode)first).getTreeNode(hash, key);
//链表结构,一直遍历直到节点为null
do {
//若当前节点满足条件就进行返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
static final int hash(Object key) {
int h;
//根据key的hashcode计算对应的hash值
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//相对于取模运算而言,运算效率高速度快
}
首先看看代码:
static final int hash(Object key) {
int h;
//根据key的hashcode计算对应的hash值
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//相对于取模运算而言,运算效率高速度快
}
答:
答:只有n = 2^x 才能满足 hash % n == (n - 1) & hash
因为每次hash时是根据对象的hashCode进行hash函数的操作,如果自定义对象作为key必须重写Object的hashCode()方法和equals()方法。这样才能保证每次相等的对象得到的hash值是一样的,防止键值改变。所以通常使用String和Integer类型作为键,因为他们内部都已经重写了hashCode()和equals()方法。
举个反例:如果不重写hashcode方法,则put时调用Object的hashcode方法,每次获取的是根据内存地址获取的。当两个不同对象具备相同值时,得到不同的hashcode,存放到不同的table下标中,所以达不到同key值进行覆盖的效果。同样在取时根据new出来的相同值的对象则取到的可能是null或其他值,无法获取之前同key值的value。
注:jdk1.7会出现扩容导致的链表死循环问题,但是jdk1.8已经解决了该问题。
该问题可以参考博客:https://www.jianshu.com/p/6be00db9ddbb