Java HashMap知识点整理

1简介
HashMap在1.7和1.8做了一个比较大的改变

在1.7之前是一个数组加链表,数据节点是一个entry节点,是它的一个内部类(头插法) : 在它resize的时候,多线程时候线程A调到 代码Entry<K,V> next => e.next执行完这段代码,线程A挂起;然后线程B开始执行transfer方法,把里面的Entry进行了rehash,B完整的执行完整个扩容流程,接着线程A唤醒,这个过程中可能造成一个链表的循环


在1.8之后 把它变成了一个链表加数组加红黑树的这个一个结构 ,把原来的一个个Entry节电变成了一个个Node节点, put过程也做了优化 ,当链表超过8时,链表就转换为红黑树(尾插法)

// transfer
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
            	//1, 获取旧表的下一个元素
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
}
// 头插法
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);
    if (size++ >= threshold)
        resize(2 * table.length);
}
Entry( int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n; //可以看出确实是把原本的链表直接链在了新建的Entry对象的后边,
        key = k;
        hash = h;
}

-- 1.8  尾插法
for (int binCount = 0; ; ++binCount) {
    //e是p的下一个节点
    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在链表中已经存在,则退出循环
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}
//如果key在链表中已经存在,则修改其原先的key值,并且返回老的值
if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}

HashMap 详解

1.7
在这里插入图片描述
1.8
在这里插入图片描述

get大致思路如下:
  1. bucket里的第一个节点,直接命中;
  2. 如果有冲突,则通过key.equals(k)去查找对应的entry
  3. 若为树,则在树中通过key.equals(k)查找,O(logn);
  4. 若为链表,则在链表中通过key.equals(k)查找,O(n)。
put函数大致的思路为:
  1. 对key的hashCode()做hash,然后再计算index;
  2. 如果没碰撞直接放到bucket里;
  3. 如果碰撞了,以链表的形式存在buckets后;
  4. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  5. 如果节点已经存在就替换old value(保证key的唯一性)
  6. 如果bucket满了(超过load factor*current capacity),就要resize。
计算hash时具体实现是这样的:
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

计算哈希的方法我们可知两点

  1. 当key为null的时候必定存放在第0位的哈希桶上
  2. 结合了hashCode(),并将其高16bit进行异或操作,使得高位都能参与计算
  3. table长度n为2的幂, 所以(n - 1) & hash 不容易碰撞,就算是发生了碰撞也用O(logn)的tree去做了
RESIZE的实现

当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。然而又因为我们使用的是2次幂的扩展,所以元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:
在这里插入图片描述
因此元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
在这里插入图片描述

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。

Fail-Fast 机制

我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了 map,那么将抛出 ConcurrentModificationException,这就是所谓 fail-fast 策略。

HashMap 常见问题

  1. 讲讲你对HashMap 的认识( 由点到面)
  2. HashMap 初始容量大小,为什么默认是16
  3. HashMap 的负载因子为什么默认为 0.75,扩容方式为什么设计成 2次幂
  4. 你知道hash的实现吗?为什么要这样实现?
发布了15 篇原创文章 · 获赞 0 · 访问量 409

猜你喜欢

转载自blog.csdn.net/u010020726/article/details/104832631
今日推荐