jdk1.7HashMap的问题

2 篇文章 0 订阅

jdk1.7的HashMap

HashMap从jdk1.8以后有较大改动,主要有两点:

  1. 插入元素改成尾插法(1.7是头插法)
  2. 链表长度超过8个转成红黑树(1.7一直是链表)

jdk为何会做这两点改变呢?

下面我们通过1.7的源码来看看这两点有什么问题或者不足之处

头插法改成尾插法

我们先来看看jdk1.7HashMap中插入元素的源码

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    //通过key计算hash值
    int hash = hash(key.hashCode());
    //通过hash值计算数组下标
    int i = indexFor(hash, table.length);
    //遍历这个下标位置的链表,如果有相同key,就替换
    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;
        }
    }
	//没有相同key,就需要插入元素,修改次数+1
    modCount++;
    //添加元素
    addEntry(hash, key, value, i);
    return null;
}

上面是put()方法,主要思路是通过添加元素的key计算出对应数组位置,如果这个位置是空的就直接走下面addEntry(),如果不是空的就遍历里面的链表,看看是否有相同key,如果有就覆盖旧值,如果没有就走下面的addEntry()。下面我们来看看addEntry()里添加元素的具体细节

void addEntry(int hash, K key, V value, int bucketIndex) {
    //e表示原链表
	Entry<K,V> e = table[bucketIndex];
    //设置新链表
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        //size+1以后如果大于阈值,就会扩容成原来的两倍
        resize(2 * table.length);
}
//原链表添加新元素==>新链表
//h=hash值,k=新元素key,v=新元素value,n=原链表
Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    //next指向了原链表,新元素插入到了原链表头部
    next = n;
    key = k;
    hash = h;
}
//扩容方法
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    //老table重新hash到新table上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    //遍历老table
    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;
                //重新计算数组下标
                int i = indexFor(e.hash, newCapacity);
                //下面三步重新组装链表挂在此下标位置
                //但是顺便颠倒了,因为采用头插法(newTable[i]=next)
                e.next = newTable[i];
                newTable[i] = e;
                //v
                e = next;
            } while (e != null);
        }
    }
}

通过上面源码看出1.7确实采用头插法,那头插法有什么问题呢?下面我们画图来解释一波

HashMap1.7

如果两个线程(线程A和线程B)同时进行扩容,线程A走到步骤①时被挂起,然后线程B走完全程,最后线程A再继续执行。因为扩容以后,元素会重新插入到新下标下,插入采用的是头插法,那么到新下标以后,链表顺序会反转,这时,如果线程并发扩容,会形成一个环形链表。

如果形成环形链表,当调用get()方法时,刚好映射到环形链表,就会出现死循环。

为了避免出现环形链表,jdk1.8开始使用尾插法。我们平时开发的时候,如果在并发环境下最好使用ConcurrentHashMap。

链表变成红黑树

我们都知道哈希表是否完美要看里面的哈希算法,但是再完美的哈希算法也不能完全避免哈希冲突,jdk中的HashMap则是采用数组加链表来解决哈希冲突,但是如果出现链表很长的情况,查询数据时,效率也会很差,因为链表的优势在于插入和删除操作,如果只是在头部或尾部插入和删除,那时间复杂度只要O(1),但是查询操作的平均时间是n/2。

如果链表长度过长,那查询效率自然也会很低。jdk1.8为了提高查询效率,引入了红黑树,因为红黑树的平均查询时间是log(n)。

为什么链表长度达到8才会转成红黑树呢?

这是因为如果长度达到8,红黑树的平均时间是log(8)=3,如果使用链表,平均时间是8/2=4;但是长度没有达到8,那么这两个查询效率差不多,而红黑树有时候需要左旋右旋等操作,也会影响效率,所以当长度没有到8时,使用链表,而且原来是红黑树,当长度小于6时,红黑树会转回链表。

最后注意一点:链表转成红黑树的前提是HashMap的长度大于64


扫一扫,关注我

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值