HashMap1.7和1.8源码解析

首先我们来说说HashMap的特点:

1、HashMap存取是无序的
2、键和值可以为null,但是键
3、键的位置是唯一的
4、JDK1.7HashMap采用的是数据结构是:数组+链表
5、JDK1.8则采用的是:数组+链表+红黑树
说到这里,我们就来看一下HashMap1.7的数组和链表是什么呢。
在这里插入图片描述
他底层使用类似这样子的数组(这个数组put的时候才会创建出来,默认长度16),数组的每个节点是一个Entry(在jdk1.8名称变成了Node),Entry代码如下

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}
//键、值、指向下一个node的指针、hash值。

因为他本身所有的位置都为null,在put插入的时候会根据key的hash去计算一个index值。
比如我现在put(”rose“,123)
put方法首先会根据key值得出一个hash值,然后在根据hash&length-1得出数组下标,假如我们的得出的index是2,如下图
在这里插入图片描述
这时候我们就成功把元素插入进来,那如果我们再put(”harden“,234),哈希本身就存在概率性,就是”rose“和”harden“我们都去hash有一定的概率会一样,这个时候就产生了所谓的哈希碰撞,hashMap处理哈希碰撞他采用了是拉链法,也就是在该位置形成一个链表并且使用了头插法。
在这里插入图片描述
我都知道1.7使用的是头插法(同一位置上新元素总会被放在链表的头部位置),那么1.8使用的是尾插法,那个使用头插法会出现什么样的问题呢?
因为1.7在resize中transfer的时候转移到新数组的链表的顺序和原数组的顺序是相反的,如果在多线程的情况下,可能会造成循环链表,那个我们如果这时候去get的话,那就悲剧了。

这里我们提到了扩容那我们就来谈一下扩容的过程吧。
有两个因素:

Capacity:HashMap当前长度(默认的长度是16)。
LoadFactor:负载因子,默认值0.75f。

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

伙伴就会疑惑了,它是怎么计算索引值的呢?

那我们就一个一个来分析一下
我们来解决第一个问题,首先我们知道16是2的4次幂,读过源码的小伙伴都知道hashMap要求容量必须为 2 次幂,其实这个设计是为了方便去取数组的下标,我们都知道源码中是采用int i = indexFor(hash, table.length);来取数组下标的,这个算法其实和取余hash%length是一样的意思,可以让数组的位置更加均匀得分配(我们都知道,二进制中2次方数只占一个bit,而我们把length-1后和hash进行与操作得出的就是hash值本身,这样的设计不是很巧妙吗)
在这里插入图片描述
如果数组长度不是2的n次幂,计算出的索引特别容易相同,也就是容易发生hash碰撞,导致数组空间分配不均匀,链表或红黑树(1.8才有)过长,这样就很影响效率了。
最后取16作为初始化容量,也是考虑了性能的问题,如果取太小了数组频繁扩容,扩容是非常耗性能的,取太大的话有浪费内存。

我们上面提到了hash&length-1和hash%length是一样的,然而hashMap用hash&length-1是一个位运算,我们知道计算机进行位运算比对十进制数计算的效率更高,所以此处也是考虑了性能问题;

接下来我们来解决第二个问题
首先我们要了解LoadFactor负载因子其实是用来衡量HashMap满的程度,表示HashMap的疏密程度,影响hash操作到同一个数组位置的概率,LoadFactor太大导致查找元素的效率低,太小导致数组利用率低,存放的数据会很分散,所以既兼顾数组利用率,又考虑链表不要太多,就选择了0.75。

JDK1.7采用的是数组+链表,即使hash函数取得再好,也很难达到元素百分百均匀分布,加入链表有n个元素,则时间复杂度为O(n),则完全是去了优势,针对这种情况,JDK1.8中引入的红黑树(查询的时间复杂度为O(log n))来优化查询的问题,链表不断变长,肯定会对查询性能有一定的影响,这时候才需要转化成树。

看过源码都知道,链表长度超过8并且数数组的长度大于等于64才会把链表转化成红黑树,那么为什么是8呢?

因为在随机哈希码下,链表节点的频率服从泊松分布,链表长度达到8的概率已经很小了,官方计算出来的值大约为0.0000006,而且红黑树节点(TresNode)占用空间是普通Node的两倍,所以选择8不是随便决定的,而是根据概率统计得来的,考虑了时间和空间的权衡。

HashMap是怎么进行扩容的呢?

1.7HashMap的扩容条件是什么?

根据以上代码,我们可以看出当size > = threshold(阈值)且null != table[bucketIndex](当前插入的位置不为空,这个条件就只有1.7JDK有)时就会进行扩容。
size表示的是整个hashMap的元素的个数,相当于(k-v键值对)的实时数量,而不是数组长度。
threshold = Capacity * LoadFactor
扩容:创建一个新的Entry空数组,长度是原数组的2倍。
转移数组:遍历原Entry数组,看过源码的都知道在transfer里面有个rehash的逻辑,但是这个逻辑在一个情况下才会去走,不走这个逻辑情况下,重新算出的index只有两个位置,一种是原来的index,一种是原来的index+旧数组的容量。

transfer(newTable, initHashSeedAsNeeded(newCapacity));
/**
     * Initialize the hashing mask value. We defer initialization until we
     * really need it.
     */
     
    final boolean initHashSeedAsNeeded(int capacity) {

        // hashSeed降低hash碰撞的hash种子,初始值为0
        boolean currentAltHashing = hashSeed != 0;
        //ALTERNATIVE_HASHING_THRESHOLD: 当map的capacity容量大于这个值的时候并满足其他条件时候进行重新hash
        boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        //TODO 异或操作,二者满足一个条件即可rehash
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            // 更新hashseed的值
            hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0;
        }
        return switching;
    }
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                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;
            }
        }
    }

1.7HashMap是怎么进行转移的呢?

通过逐个&&(length-1),然后进行一次一次的转移。

1.8HashMap的扩容条件是什么?

当链表大度大于8 && 数组长度<最大数组长度64 会进行扩容

 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
 static final int MIN_TREEIFY_CAPACITY = 64;

当个数大于阈值也会进行扩容

if (++size > threshold)
            resize();

1.8HashMap是怎么进行转移的呢?

在1.7HashMap的基础上发现,索引扩容后只有两种情况,哪两种呢?
一种是原索引不变,另一种是原索引+旧数组容量,在源码中的体现。

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;
 }

使用loHead和hiHead记录,一次性进行转移。
最后我画了个思维图。
在这里插入图片描述

总结

HashMap jdk1.7和1.8有什么不同?

1、 jdk1.7 : 数组+链表 jdk1.8 : 数组+链表+红黑树
2、 jdk1.7 hash算法更复杂,散列性更高,查询效率更高
Jdk1.8中增加了红黑树,查询效率得到了保障,所以可以简化hash算法,毕竟hash算法消耗cpu。
3、 jdk1.7有根据hashseed(哈希总值)是否变化进行重新hash,jdk1.8没这过程。
4、 扩容条件不同:jdk1.7扩容条件是1、数组长度大于阈值2、有产生hash碰撞。
Jdk1.8的扩容条件是1、数组长度大于阈值 || 2、链表长度大于8且数组长度<64。
5、 扩容转移不同:jdk1.7是一个一个进行转移。Jdk1.8 是判断hash值与上旧容量,哪些为0,哪些不为0,索引为原索引+旧数组容量,然后一次性转移。
6、jdk1.7采用尾插法,在扩容时候会导致循环链表问题,导致数据丢失。
Jdk1.8 采用尾插法解决这个问题。
7、jdk1.8新增api,putIfAbsent(key,value),在源代码中if(!onlyIfAbsent)体现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值