HashMap源码解析
1.1Hash算法
核心原理:Hash也称散列、哈希。基本原理就是把任意长度的输入,通过Hash算法变为固定长度输出
Hash的特点
1.从Hash值不可以反向推导出原始的数据
2.输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
3.哈希算法的执行效率要高效,长的文本也能快速的计算出哈希值
4.hash算法的冲突概率要小
由于hash的原理是将输入空间的值映射成hash空间内,而hash值的空间远小于输入的空间,根据抽屉原理
,一定会存在不同的输入被映射成相同输入的情况,即哈希冲突
抽屉原理 桌子上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们发现至少会有一个抽屉里面放不少于两个的苹果,这一现象就是我们所说的抽屉原理
1.2常量、属性与构造器
常量
//默认长度16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大长度 1 << 30 即 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认0.75 当哈希表长度使用了0.75后就会扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化条件1 链表长度大于8
static final int TREEIFY_THRESHOLD = 8;
//红黑树降级为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//树化条件2 哈希表长度大于64 结合TREEIFY_THRESHOLD两个条件都满足链表才会树化
static final int MIN_TREEIFY_CAPACITY = 64;
属性
//哈希表 是一个Node类型的数组 只有第一次put操作时才会初始化table 默认长度是16
transient Node<K,V>[] table;
//每一个Node构成的一个Set集合
transient Set<Map.Entry<K,V>> entrySet;
//哈希表元素个数
transient int size;
//当前哈希表结构修改次数(增(修改不会)、删会改变此值)
transient int modCount;
//扩容阈值 当哈希表中的元素个数大于 (数组长度 * 负载因子)时 就会扩容
int threshold;
//负载因子 默认就是0.75
final float loadFactor;
Node类
//是HashMap的一个静态内部类 实现了Map接口中的Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key的哈希值
final K key; // key
V value; //value
Node<K,V> next; //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;
}
//equals()方法
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;
}
}
构造器
双参构造器
//传入初始容量 与 加载因子
public HashMap(int initialCapacity, float loadFactor) {
//校验
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//阈值一定是2的次方数
this.threshold = tableSizeFor(initialCapacity);
}
||
||
tableSizeFor(int cap)
//返回一个大于等于(等于的情况就是传来的数就是2的次方数 操作之后还是原数)当前值cap的一个数,并且这个数一定是2的次方数
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;
}
空参构造器
//空参构造器只将负载因子初始化为默认的0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
传入一个Map构造器
//将一个Map中数据复制到新的Map中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
1.3put()方法
底层调用了putVal()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
调用putVal()时,先调用了hash(key)的方法对key的哈希值做了一次rehash操作
/**将key的hashCode的低16位 ^ key的hashCode的高16位
* 目的: 因为寻址算法是key的 (哈希值 & table.length - 1) 一般情况下,table的 * length都是很小的,这样的话key的哈希值的高位就没有参数到运算中,容易造成哈希冲突
* 让高16位也参数运算,可以使哈希值更加散列,可以减小哈希冲突。
*
* 此外,我们也看到当key为null时,哈希值就是0,即将会放到table1的索引为0的位置
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab 引用当前的hashMap的散列表
// p 表示散列表当前位置的元素
// n 表示散列表数组的长度
// i 寻址得到的下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
//当tab为null 或者长度为0时 去创建散列表
//延迟初始化 第一次调用putVal方法时底层才会将散列表创建出来
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //初始化散列表 为n赋值
----------------------------put数据情况----------------------------------------
/*
* 寻址算法就是 (table.length - 1) & hash
* 根据前面我们知道,table.length一定是2的n次幂 2的n次幂的二进制有一个特点就是
* xxx10000(一个1其他全都是0),当它减1后,就会变为 ...0001111,即高位都是0低位全部变为 * 1,这种数进行&运算得到的结果就是 [0, table.length - 1] 刚好对应了数组中的下标。实 * 际上就是一个取模运算,只是位运算的效率较高一些。
*/
//情况1 寻址后当前位置没有元素 直接newNode()方法构造一个node放到当前位置即可
if ((p = tab[i = (n - 1) & hash]) == null) //这里对p进行了赋值
tab[i] = newNode(hash, key, value, null);
//下面的情况都是当前位置上已经有元素了 即p不为null
else {
// e 不为null的话 找打了一个
// k 表示临时的一个Key
Node<K,V> e; K k;
//情况2. 表示当前位置的元素与待插入元素的key完全一致 将p赋值给e 需要进行后续的替换操作
//要插入的元素的hash值与p的hash值相等 && (两者key相等 || 两者key的equals结果相等)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//情况3.、 当前位置已经形成了红黑树了 将其添加到树里
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//情况4. 当前位置形成了链表 遍历整个链表,如果找到key完全相同的就替换,没找到就插入到链表尾部 (1.7头插 1.8尾插防止形成环)
else {
for (int binCount = 0; ; ++ binCount) {
if ((e = p.next) == null) {
//添加到尾部
p.next = newNode(hash, key, value, null);
//检查是否符合树化标准 即链表长度大于8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //树化操作
break;
}
//比较key的hash值与equals 找到了一个key完全相同的元素
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//value进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue; //直接return了 不执行下面的逻辑
}
}
//表示散列表被修改的次数 插入新元素才会++,替换元素逻辑直接return了,不 ++
++modCount;
//++size后 大于扩容阈值 触发扩容逻辑
if (++size > threshold)
resize(); //扩容
afterNodeInsertion(evict);
return null;
}
put方法总结
1、先判断哈希表是否被初始化过,如果没有初始化过,先初始化哈希表。
2、根据寻址方法 (hash & (table.length - 1)) 找到对应的存放索引
3、存放元素。
- 1.当前位置没有元素,直接放
- 2.当前位置有元素,且待插入元素的key与当前位置元素的key完全一致,后续进行value的 替换
- 3.当前位置已经形成红黑树了,遍历树中的节点,如果找到key完全一致节点,后续进行替换,否则插入到树中。
- 4.当前位置形成链表了,遍历链表如果找到key完全一致的后续也进行替换value,否则插入链表中(尾插法),并检查当前插入完元素后是否达到了树化标准
(链表长度大于8 并且数组长度大于64)
4、添加完元素后(非修改value),size++,此时会检查是否size是否大于扩容阈值(threshold),大于的话会触发resize()逻辑。
1.4resize()方法
为什么需要扩容? 为了解决哈希冲突导致的链化影响查询效率的问题,扩容会缓解该问题。
final Node<K,V>[] resize() {
//引用扩容前的哈希表
Node<K,V>[] oldTab = table;
//oldCap 表示扩容之前哈希表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//扩容前的阈值
int oldThr = threshold;
//newCap扩容之后的哈希表长度 newThr扩容后的阈值
int newCap, newThr = 0;
//oldCap > 0 说明哈希表已经被正常初始化过了,这是一次正常的扩容操作
if (oldCap > 0) {
//oldCap比规定的长度最大值都大 (1 << 30)
if (oldCap >= MAXIMUM_CAPACITY) {
//阈值变为int最大值 2 ^ 31 - 1
threshold = Integer.MAX_VALUE;
//直接返回原哈希表
return oldTab;
}
-------//普遍的扩容
//1、newCap必然变为oldCap的两倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果说扩容完的长度小于规定的最大值 && 原哈希表长度大于等于16时扩容阈值也翻倍 否则扩容阈值不翻倍
newThr = oldThr << 1; // double threshold
}
/*
* oldCap == 0 但是 oldThr > 0 使用下面三种构造器创建HashMap时会初始化阈值
* 1. new HashMap(initCap, loadFactory)
* 2. new HashMap(initCap);
* 3. new HashMap(map) 这个map中有数据
*/
else if (oldThr > 0) // initial capacity was placed in threshold
//前面分析过构造器 这个阈值一定是2的次幂 所以初始化容量也一定是2的次幂
newCap = oldThr;
/*
* oldCap == 0 && oldThr == 0
* 调用了 new HashMap()方法
*/
else {
newCap = DEFAULT_INITIAL_CAPACITY; //newCap赋值为16
//newThr = 默认的加载因子0.75 * 默认的初始容量16 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果newThr为0时,通过newCap和loadFactor计算出一个newThr
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //将上面赋值完的newThr赋值给threshold
//上面都是计算newCap 和 newThr的值
------------------------------------------------------------------------------ //下面都是扩容操作 就是遍历原哈希表拿出每一个节点在新的哈希表中进行重新寻址重新put的情况
@SuppressWarnings({"rawtypes","unchecked"})
//根据上面计算出来的newCap构造一个新的哈希表
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍历原哈希表
for (int j = 0; j < oldCap; ++j) {
//原哈希表位置的节点
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//只有单个元素 使用寻址公式重新寻址
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//树化了
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//形成了链表
else { // preserve order
//低位链表:存放在扩容之后的哈希表的下标位置与原哈希表的下标索引位置一样
Node<K,V> loHead = null, loTail = null;
//高位链表:存放在扩容之后的哈希表的下标位置与原哈希表的索引位置不一样
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/*
假设原哈希表长度为16 原节点存放在索引为15的位置,即原节点的哈希值的后4位一定是1111
而第5位可能是0 即01111,也可能是1 即11111
扩容后的哈希表长度为32 如果是 x & 16(10000) == 1 则x的第五位是1 否则是0
0的话 & (32-1) 还是15即放在原索引 否则就不放在原索引位置
*/
//hash值 & 原长度
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; //将末位置为null
newTab[j] = loHead; //放到原位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
扩容流程
1、计算出newCap(哈希表新的长度,普遍情况是oldCap << 1,即扩容为原来的两倍)
和newThr(新的扩容阈值)
2、根据newCap创建出一个新的哈希表。
3、遍历原哈希表中的每一个节点,根据寻址方式(hash & table.length - 1)
重新计算索引,分三种情况。
- 1.哈希表的原位置只有一个元素,直接根据寻址方式计算新的索引位置即可。
- 2.原位置已经是树结构了,遍历树中的节点根据寻址方式计算新的索引。
- 3.原位置是链表结构,根据位运算,链表中的的一部分节点经过寻址后的索引位置和原哈希表的索引位置一致,一部分是不一致的。遍历链表,将两部分进行分离,然后存放到新的哈希表中。
1.5get()方法
get()方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode()
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) {
//刚好第一个位置就是要找的Node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//当前桶位不止一个元素
if ((e = first.next) != null) {
//当前桶位已经形成树了 去树中找Node
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;
}
1.6remove()方法
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
removeNode方法
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
/*
* tab 引用当前的哈希表
* p 当前位置的元素
* n 散列表的长度
* index 寻址索引
*/
Node<K,V>[] tab; Node<K,V> p; int n, index;
//判断哈希表是否为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//-------------------查找过程跟getNode方法一致--------------------------------
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;
//p在node的前面 链表的删除 p.next = node.next
else
p.next = node.next;
++modCount; // modCount + 1
--size; // size - 1
afterNodeRemoval(node);
return node;
}
}
return null;
}
1.7replace()方法
replace()
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
//判断是否存在指定的key和value
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
//将新值进行替换
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}