HashMap-源码
# put
HashMap map =new HashMap(2);
map.put("li",1);
map.put("c",2);
map.put("c",3);
# JDK1.8源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//transient Node<K,V>[] table;第一个if里面有个table,这个是存放map的数组
//用于下面if里面进行赋值
Node<K,V>[] tab;
//节点,用于临时存放map里的节点
Node<K,V> p;
//n 数组大小,i 插入元素在数组中的下标位置
int n, i;
//tab = table进行赋值,判断tab是否空,如果空,进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
//进行数组初始化大小 n
n = (tab = resize()).length;
//(n - 1) & hash] 通过hash运算找到插入元素应该放到的数组下标
//p = tab[i = (n - 1) & hash]将数组这个下标的元素赋值给p
//判断p是否是空,代表没有元素,没有hash冲突
if ((p = tab[i = (n - 1) & hash]) == null)
//直接创建一个新节点,放到数组中
tab[i] = newNode(hash, key, value, null);
else {
//代表这个数组已经有元素了,需要解决hash冲突
//说明下面的数据是链表或者红黑树,创建Node<K,V> e来临时存放元素
Node<K,V> e;
K k;
//判断桶里的元素和插入元素的key和hash是否都相等,这里判断的是tab[i]
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果相等,进行覆盖
e = p;
//不相等的情况就新增
//判断链表是否为红黑树
else if (p instanceof TreeNode)
//如果是红黑树,直接进行put添加
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);
//插完之后,如果链表元素个数大于等于8,链表转换成红黑树
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))))
//如果相等,就跳出,让下面if (e != null) { 开始覆盖
break;
// 说明没有hash冲突,继续遍历链表吧
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
//桶里的元素不为空
if (e != null) { // existing mapping for key
//记录一下桶里的元素
V oldValue = e.value;
//onlyIfAbsent为false:改变原来的值
//oldValue == null 旧值为空
if (!onlyIfAbsent || oldValue == null)
//覆盖
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//修改++
++modCount;
//判断数组是否需要扩容
//size:The number of key-value mappings contained in this map.链表长度
//threshold:The next size value at which to resize (capacity * load factor).
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
# resize扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//原数组大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧阈值点 默认是12(16*0.75)
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// static final int MAXIMUM_CAPACITY = 1 << 30;数组最大容量,如果超过了,就不扩容了,你就去碰撞吧,我也没法
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
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;
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;
}
# 为什么说hashmap的扩容机制很妙?
# 1.位运算
HashMap使用位运算来计算hash值与数组长度之间的关系。HashMap内部,数组的长度总是2的幂次方,这使得数组的下标计算更加高效。通过哈希值的进行位与运算(hash&(length-1)),可以将哈希值映射到数组的索引位置上,由于数组长度总是2的幂次方,所以length-1的二进制表示所有的位都是1,这样就保证了位与运算的结果为哈希值的低位,从而实现快速定位。
1.为什么数组的长度是2的幂次方,数组下标的计算会更高效?2.length-1的二进制表示所有的位都是1为什么?3.哈希值的低位,从而快速定位,为什么?
- 数组的长度选择为2的幂次方是为了利用位运算来计算元素的索引位置,这样可以提高计算效率。假设数组的长度为2的幂次方n,那么n-1的二进制表示为所有低位都是1,高位都是0。这样,在使用哈希值对数组长度取模时,就相当于对哈希值的低位进行了取模操作,这样可以确保散列到各个位置的概率均等,减少了碰撞的可能性。
- 当数组的长度为2的幂次方n时,n-1的二进制表示中的所有位都是1。这是因为一个2的幂次方减去1的结果,其二进制表示中的每个位均为1。这样做有利于通过位运算来计算元素的索引位置,提高了计算效率。(参考下方进制对照表)
- 哈希值的低位被用来快速定位是因为在进行取模运算时(%操作),取模运算实际上就是保留了除数的低位,而丢弃了高位。当数组长度为2的幂次方时,取模运算实际上就是对哈希值进行与(&)操作,这样可以直接使用哈希值的低位来确定元素存放的位置,而不需要进行昂贵的除法运算。这种技巧可以提高散列表的性能,特别是在处理大量数据时。
# 2.新增位0不变
新增位为0不变:在HashMap的扩容过程中,当一个元素需要插入到已有的数组中时,跟进元素的哈希值重新计算在新数组中的索引位置,如果新数组的长度是原数组的两倍,那么新数组的长度的二进制表示中只有最高位不同,其他位都是相同的,因此,在重新计算索引位置时,只需要判断原索引位置的最高位是0还是1,如果是0,则索引位置不变 ,如果是1,则在原索引位置的基础上加上原数组的长度的值。这个操作巧妙的运用的位运算,避免的大量元素的重新计算和拷贝,提高了扩容的效率。
# 举个例子
当HashMap的容量达到一定阈值时,就会触发扩容操作。下面以一个简单的例子来详细说明HashMap的扩容过程。
假设我们有一个HashMap,初始容量为8,负载因子为0.75(即在HashMap中存储的元素数量超过容量的75%时,就会进行扩容)。现在我们向HashMap中插入5个元素,它们的哈希值分别为3、11、19、27和35(为了方便说明,这里直接使用元素的哈希值作为示例)。
初始状态:
- 容量:8
- 元素数量:0
插入第一个元素(哈希值为3):
- 根据哈希值计算索引位置:3 & (8-1) = 3
- 在索引位置3处插入元素
插入第二个元素(哈希值为11):
- 根据哈希值计算索引位置:11 & (8-1) = 3
- 发生了冲突,需要处理冲突
- 发现索引位置3已经被占用,通过链表方式存储冲突的元素
- 在索引位置3的链表尾部插入元素
插入第三个元素(哈希值为19):
- 根据哈希值计算索引位置:19 & (8-1) = 3
- 发生了冲突,需要处理冲突
- 发现索引位置3已经被占用,通过链表方式存储冲突的元素
- 在索引位置3的链表尾部插入元素
插入第四个元素(哈希值为27):
- 根据哈希值计算索引位置:27 & (8-1) = 3
- 发生了冲突,需要处理冲突
- 发现索引位置3已经被占用,通过链表方式存储冲突的元素
- 在索引位置3的链表尾部插入元素
插入第五个元素(哈希值为35):
- 根据哈希值计算索引位置:35 & (8-1) = 3
- 发生了冲突,需要处理冲突
- 发现索引位置3已经被占用,通过链表方式存储冲突的元素
- 在索引位置3的链表尾部插入元素
此时,HashMap的状态如下:
- 容量:8
- 元素数量:5
- 索引位置3上有一个包含5个元素的链表,元素的顺序为3 -> 11 -> 19 -> 27 -> 35
扩容操作触发:
- 当元素数量达到容量的0.75倍时,触发扩容
- 容量翻倍,变为16
- 创建一个新的容量为16的数组,将旧数组中的元素重新分配到新数组中
重新分配元素:
- 遍历旧数组中的元素,根据元素的哈希值计算在新数组中的索引位置
- 哈希值为3的元素,新索引位置仍然为3,放在新数组的索引位置3处
- 哈希值为11的元素,新索引位置为11 & (16-1) = 11,放在新数组的索引位置11处
- 哈希值为19的元素,新索引位置为19 & (16-1) = 3,发生冲突
- 发现索引位置3已经被占用,通过链表方式存储冲突的元素
- 在索引位置3的链表尾部插入元素
- 哈希值为27的元素,新索引位置为27 & (16-1) = 11,发生冲突
- 发现索引位置11已经被占用,通过链表方式存储冲突的元素
- 在索引位置11的链表尾部插入元素
- 哈希值为35的元素,新索引位置为35 & (16-1) = 3,发生冲突
- 发现索引位置3已经被占用,通过链表方式存储冲突的元素
- 在索引位置3的链表尾部插入元素
此时,HashMap的状态如下:
- 容量:16
- 元素数量:5
- 索引位置3上有一个包含3个元素的链表,元素的顺序为3 -> 19 -> 35
- 索引位置11上有一个包含2个元素的链表,元素的顺序为11 -> 27
通过这个例子,我们可以看到在HashMap扩容过程中,通过重新计算哈希值的索引位置,将元素重新分配到新的数组中。对于哈希值相同的元素,会通过链表方式存储,保持了元素之间的顺序。这样,HashMap的扩容操作能够保证元素的迁移效率,并且保持了元素的相对顺序。
# ConcurrentHashmap和hashmap的区别
- 线程安全性
- HashMap线程不安全、ConcurrentHashMap是线程安全的。如果多个线程同时操作一个 HashMap 对象,可能会导致数据不一致或者抛出 ConcurrentModificationException 异常。
- ConcurrentHashMap使用锁分段技术(Segment)(jdk1.7)或CAS(jdk1.8)保证线程安全。不同的部分可以被不同的线程同时访问,从而提高了并发性能。
- 并发性能
- 多线程环境下,ConcurrentHashMap 的性能通常优于 HashMap,因为它允许多个线程同时访问数据,而写操作只会锁住对应的部分,不会影响其他部分的读操作
- HashMap在多线程环境下需要自行保证线程安全,要额外的同步手段,会降低性能
- 扩容机制
- HashMap在扩容时需要重新计算hash,重新分布数据,整个过程非线程安全,可能导致链表的循环引用问题
- ConcurrentHashMap在扩容时,只需锁住部分,其他部分依然可以被访问,因此扩容时性能更好
# HashMap扩容可能出现循环引用问题
在HashMap的扩容过程中,由于需要重新计算哈希值并重新分布数据,可能会导致链表循环引用的问题。具体来说,当存在多个元素哈希值映射到同一个索引位置,并形成链表时,在扩容过程中,如果没有正确处理链表节点的引用关系,就可能导致链表的循环引用。
举个例子来说明这个问题:
假设我们有一个初始容量为8的HashMap,负载因子为0.75,存储的元素如下:
- 元素A的哈希值为3,存储在索引位置3
- 元素B的哈希值为11,存储在索引位置3,并与元素A形成链表
此时,HashMap的状态如下:
- 容量:8
- 元素数量:2
- 索引位置3上有一个包含2个元素的链表,元素的顺序为A -> B
当元素数量达到容量的0.75倍时,触发扩容操作。容量翻倍,变为16,新建一个容量为16的数组,并将旧数组中的元素重新分配到新数组中。
在重新分配元素的过程中,我们需要遍历旧数组中的链表,并根据元素的哈希值计算在新数组中的索引位置。假设在索引位置3的链表上进行处理,我们需要重新计算元素B的哈希值,并尝试将其放入新数组中。
如果在重新计算哈希值的过程中,没有正确处理链表节点的引用关系,就可能出现链表循环引用的问题。具体来说,假设重新计算元素B的哈希值后,它的新索引位置仍然为3。如果在将元素B插入新数组的索引位置3时,没有断开旧数组中元素B与元素A之间的引用关系,就会导致链表循环引用的问题。
这样,新数组中的索引位置3上的链表会变成一个循环链表,元素A的下一个节点是元素B,元素B的下一个节点是元素A,形成了一个闭环。这种循环引用会影响HashMap的正常操作,可能导致无限循环、数据丢失等异常情况发生。
为了解决这个问题,HashMap在扩容过程中需要正确处理链表节点的引用关系,确保在重新分配元素时不会产生链表循环引用的问题。
进制对照表: