java-HahMap相关问题
概述
文章对HashMap的部分细节进行介绍,JDK1.7之前有可能出现环形表的问题,而1.7之后进行了改进,文章对环形表现象的出现进行了解析,然后对HashMap注意的几个问题进行了解答。
HashMap的底层实现是数组,主要具有以下特点 :
- 键值对都允许为空(重要)
- 线程不安全
- 不保证有序
jdk1.7 迁移过程
首先看一下数据迁移的地方在哪里?JDK1.7 HashMap
public V put(K key, V value)
{
......
//算Hash值
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
//如果该key已被插入,则替换掉旧的value (链接操作)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//该key不存在,需要增加一个结点
addEntry(hash, key, value, i);
return null;
}
//增加一节点
void addEntry(int hash, K key, V value, int bucketIndex)
{
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
if (size++ >= threshold)
resize(2 * table.length);
}
//重新生成空间
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//创建一个新的Hash Table
Entry[] newTable = new Entry[newCapacity];
//将Old Hash Table上的数据迁移到New Hash Table上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
//数据迁移过程
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next; //NO.1
int i = indexFor(e.hash, newCapacity); //NO.2
e.next = newTable[i]; //NO.3 把当前位置的节点放在新插进来节点的next(于是这里的当前位置在下面一步就会变成久节点了)
newTable[i] = e; //NO.4 当前位置的节点放新插入的节点
e = next; //NO.5 e 换成链表中的下一位
} while (e != null);
}
}
}
正常的情况下,下图可以看见久的往后移,新的往前插(饭堂排队吃饭插队一样)
并发情况下,假如有两个线程执行,
- 线程1执行到 NO.1时
- 线程2执行完了
就会出现如下
jdk1.8 迁移过程
/* ---------------- Fields -------------- */
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*
* 存放数据的结构 ,常以两倍大小扩容
*
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*
* 方便遍历用的
*
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
*
* 数量
*
*/
transient int size;
/**
*
* 在 JDK 1.8 中,HashMap 中的 modCount 字段是用于记录 HashMap 结构修改次数的。具体来说,modCount 字段记录了 HashMap 中插入、删除、扩容等操作的次数,每进行一次这样的操作,modCount 字段的值就会自增 1。这个字段主要用于实现 HashMap 的迭代器(Iterator)和 Spliterator,以便在迭代过程中检测 HashMap 是否被修改。
* 在 JDK 1.8 中,HashMap 的迭代器(Iterator)和 Spliterator 都是快速失败(fail-fast)的,即在迭代过程中如果发现 HashMap 被修改了,就会抛出 ConcurrentModificationException 异常,以保证迭代器的正确性。为了实现快速失败机制,HashMap 在迭代器和 Spliterator 中都需要检测 modCount 字段是否与迭代器或 Spliterator 创建时的值相等,如果不相等就抛出 ConcurrentModificationException 异常。
* 需要注意的是,modCount 字段并不是线程安全的,如果多个线程同时对 HashMap 进行修改,可能会导致 modCount 字段的值不一致。因此,在多线程环境下使用 HashMap 时,应该使用线程安全的 ConcurrentHashMap 类,以避免这种问题。
*
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* 下次扩容的大小
*
*
* @serial
*/
int threshold;
/**
* The load factor for the hash table.
*
* 负载因子
*
* @serial
*/
final float loadFactor;
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) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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 {
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 后续的调整大小应该会走到这里
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];
// 把新创建的 tab 赋值给原来的那个 table
table = newTab;
if (oldTab != null) {
// 这是扩容后 , 迁移原有 tab 上的元素
// 变量原有数组上的元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 1. next 指针为空表示那个槽位只有一个元素 , 直接放在新的位置(重新计算位置)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 2. 假如是红黑树 ,那么插入到红黑树那里去
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 3. 都不是, 那肯定就是链表上的 node
// 链表的迁移分两部分, 一部分是 hash 值和旧空间大小与 == 0 的 , 不移动位置 ; 其余的移动位置
// 下面是移动位置
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;
}
resize 操作有点难懂 ,用下面这张图就明白了
问题
图片来源与参考资料。
哈希表如何解决Hash冲突
JDK1.7之后换成了尾插法,不会出现环形表的但是依然是线程不安全的。
HashMap 中的 key若 Object类型, 则需实现哪些方法?
HashMap 扩容时机及扩容时避免rehash的优化
每次增加节点的时候 ,都会检查一下容量 ,当达到一个阈值的时候就会触发扩容机制 ,扩容时避免rehash的优化
可以增加一下负载因子 , 提高空间利用率 .
为什么使用红黑树
- 使用红黑树可以降低树高
- 红黑树增删改查平均时间复杂度为log(n) , 且时间复杂度最差情况都是log(n)
那为什么不使用平衡二叉树
主要原因是红黑树的平衡性能更好。红黑树是一种自平衡的二叉查找树,它能够在O(log n)的时间复杂度内完成插入、删除和查找等操作。相比之下,平衡二叉树需要对每个节点计算平衡因子,以判断是否需要进行旋转等操作,因此在实现和维护上相对复杂,性能也相对较低。(红黑树旋转次数比平衡二叉树少)
HashMap,HashTable,ConcurrentHashMap的共同点和区别?
相同点 : 内部底层数据结构都是数组 , 都会扩展 ,都是利用hash 确定位置
区别 : HashMap 不是线程安全的;HashTable 和 ConcurrentHashMap则是线程安全的; HashMap允许 null 值 和 null 键 而 HashTable 不允许 ;
hashmap 为什么是线程不安全的
头插法指针在指向的时候有可能形成回环, Java8以后改进成尾插法 ,但是依旧不是线程安全的
参考资料
这是之前写过的博客