java HashMap源码分析
hashMap几个万恶的问题:咱们挨个看源码解答,这玩意除非你看源码,不然谁知道。。。
- key是否可以为null? 答:可以,看put方法以及hash方法即可
- hashMap什么时候扩容? 答:当size > threshold时进行 resize
- hashMap是否是线程安全的? 答:线程不安全,主要方法并没有synchornized关键字修饰
- hashMap的容量以及扩容为什么都是2的倍数? 答:详情看 index = (lenght-1) & hash这个计算索引的方式
hashMap成员变量:与自身容量相关
//默认初始容量,2的4次方=16,必须是2的倍数,单看这行你看不出来为啥要是2的倍数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方,int有32位,java中最高位是符号位,剩下31位,是2的0-30次方共占用31位
//所以最大就是2的30次方,这最大的还是2的倍数,这是个很神奇的事情
static final int MAXIMUM_CAPACITY = 1 << 30
//默认的容量系数,和容量有关
static final float DEFAULT_LOAD_FACTOR = 0.75f
hashMap成员变量:与树化相关
//树化的临界值
static final int TREEIFY_THRESHOLD = 8;
//解除树化的临界值
static final int UNTREEIFY_THRESHOLD = 6;
//最小树化的容量
static final int MIN_TREEIFY_CAPACITY = 64;
hashMap成员变量:与存储相关
//数据节点,这是个链表,看到nextNode没有
static class Node<K,V> implements Map.Entry<K,V>{
final int hash;
final K key;
V value;
Node<K,V> next;
};
//节点数组,数组中每个节点又是一个链表
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
主要方法:构造方法
//构造方法
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//即时你传递的值大于2的30次方,也没用,最大就是2的30次方
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
主要方法:put方法
//最常用的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
主要方法:hash方法
//hash值计算,这里看到Key为Null时,hash值为0
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
主要方法:putVal方法
- 根据hash与数组长度计算index值
- index处是否有节点存在,空则直接存入Index处
- index处有节点,判断节点是否是树型节点,是则直接在树中追加节点
- 非树节点,则根据index节点的.next属性遍历找到末尾节点进行追加,且判断长度
- 当长度大于等于7时,则进行树化操作
- 当所有node的数量 > threshold时,进行扩容
//final 修饰的方法不能被重写
//boolean onlyIfAbsent : true则不改变任何值,false:则进行覆盖,当key相同时有用
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;
Node<K,V> p; -> 等于插入成功后的值,或者插入前就占在这个位置上的值
int n; -> table[].length
int i; -> index = (length-1)&hash
//判断 transient Node<K,V>[] table 这个属性是否需要初始化
//Node<K,V>[] table = new Node[DEFAULT_INITIAL_CAPACITY]
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}
//p=tab[index]; index=(table.length-1) & hash,这步是容量为2的倍数的关键!!!
if ((p = tab[i = (n - 1) & hash]) == null){
//如果当前index处没有值,创建新的Node
tab[i] = newNode(hash, key, value, null);
} else {
//如果当前index有值,这就是传说中的hash碰撞了
Node<K,V> e; -> key相同的占位节点,或者占位的树类型节点
K k; -> 当前占位的这个元素的key
//这就是你存入了两个相同的key了,hash值一样,key的值也一样
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
} else if (p instanceof TreeNode) {
//如果当前占位节点已经是树节点了,就要以树的形式追加一个节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
//非树节点,则以链表的形式追加在末尾,currentNode.next = newNode
//binCount 等同于当前占位节点后面的链表长度,这条链每个节点的key的hash都相同
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//找末尾节点,末尾节点的next为空,追加在末尾节点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
//如果这条链的长度>=(8-1=7)了,则将这条链进行树化
treeifyBin(tab, hash);
}
break; //不够树化的数量呢,链表追加完退出循环,完事收工
}
//这条链上的每个节点,都与要插入的节点判断下key是不是相同,相同的则不做任何处理
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
//e不管是树还是链表,当e不为空时,意味着要插入的节点与e,他们key的hash值完全相同了
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
} // else 到此结束
++modCount;
//这一步的判断是扩容的关键,需要展开分析
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//才发现,put方法其实返回的是null
return null;
}
树化过程:先记好与树相关的几个成员变量
static final int TREEIFY_THRESHOLD = 8; -> 用于与hash冲突的链表做比较
//解除树化的临界值
static final int UNTREEIFY_THRESHOLD = 6; -> 用于与hash冲突的链表做比较
//最小树化的容量
static final int MIN_TREEIFY_CAPACITY = 64; -> 用于与table.length做比较
//节点数组
transient Node<K,V>[] table;
树化相关代码:
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
树化的具体实现:treeifybin
//参数说明:tab[] hashMap的实例属性,存放所有node的数组
//参数说明:hash 你当前put时传入的key所算出的hash值
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n;
int index;
Node<K,V> e;
//用到了树化的第二个参数,当数组长度小于64时,进行重新分配大小,但不进行树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
/**获取hash值相同的e节点,能够执行到这个方法里的,说明当前这个e节点的长度
* 已经大于等于 7 了,且e节点是链表第一个节点
*/
TreeNode<K,V> hd = null; //hd是一个双向链表的头节点,节点是TreeNode
TreeNode<K,V> tl = null; //t1是temp节点,用于临时保存节点
do {
/**
* 根据第一个节点e的值,创建了一个新的节点p,且节点p的后继节点为空,
* 根据do->while循环,e节点分别为链表上的,第1,2,3...7节点
* 且根据这些节点,每次循环都创建一个节点p进行操作
*/
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null){
/**
* 第一次循环链表的第一个节点时,t1才为null,此时p是根据第一个节点
* 创建的TreeNode,hd=p,说明hd是这个链表的头节点
*/
hd = p;
} else {
//t1是上一个节点,p是当前节点,这两行代码创建了一个双向的链表
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}