HashMap1.7和1.8源码解析

1,513 阅读6分钟

HashMap1.7和1.8区别

  • 数据结构
  • 插入数据方式
  • hash计算规则(扩容时数组存储位置)不一样
  • 扩容与插入数据顺序

1.7 

  1. 数组+链表的数据结构

  2. 插入数据时使用头插法(新来的值会取代原有的值,原有的值就顺推到链表中去)。

  3. 扩容时数组存储位置****hash值和需要扩容的二进制数进行&,h & (length-1)

  4. 插入数据前扩容

1.8 

  1. 数组+链表+红黑树的数据结构。**当链表的长度达到8,也就是默认阈值以及table长度大于 MIN_TREEIFY_CAPACITY(节点被树化时的最小的hash表容量)时。**自动扩容把链表转成红黑树来把时间复杂度从O(n)变成O(logN)提高了效率)
  2. 插入数据时使用尾插法
  3. 低位原始位置,高位扩容前的原始位置+扩容的大小  高位:newTab[j + oldCap] = hiHead; 低位newTab[j] = loHead;
  4. 插入数据成功后扩容

为什么把头插法改为尾插法

1.7在多线程操作HashMap时可能引起死循环,原因是扩容转 移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。
1.8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。

**1.8中HashMap把链表转化为红黑树的阈值是8,**红黑树化为链表为6

如果选择6和8(如果链表小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。

源码部分就那put方法来展示,put方法比较具备代表性。

HashMap中的常量

  • DEFAULT_INITIAL_CAPACITY = 1 << 4; HashMap的默认容量 16

  • MAXIMUM_CAPACITY = 1 << 30; HashMap的最大容量 2^30

  • DEFAULT_LOAD_FACTOR = 0.75f; 负载因子。默认0.75.扩容时使用

  • TREEIFY_THRESHOLD = 8; 数组某个下标对应的链表长度大于该默认值,转化为红黑树

  • UNTREEIFY_THRESHOLD = 6; 数组某个下标对应的链表长度小于该默认值,转化为链表

  • MIN_TREEIFY_CAPACITY = 64; 节点被树化时的最小的hash表容量(当桶中node的数量大到需要变为红黑树时,若hash表的容量小于MIN_TREEIFY_CAPACITY容量时需要进行resize()扩容。操作MIN_TREEIFY_CAPACIT的值至少是TREEIFY_THRESHOLD 的4倍)

  • Entry<K,V>[] table / Node<K,V>[] table; 存储元素的数组。总是2的n次幂

  • Set<Map.Entry<K,V>> entrySet; 存储集体元素的容器

  • int size; 存储键值对的数量

  • int modCount; hashMap扩容和结构改变的次数

  • int threshold; 扩容的临界值(容量 * 负载因子)

  • float loadFactor; 负载因子

JDK7中的HashMap

说一下jdk7的版本jdk1.7.0_80。虽说都是1.7,但是代码还是不太一样。

初始化HashMap map = new HashMap<>();    

实例化的时候校验了初始化容量和负载因子。并没有去初始化table数组

put方法源码

**map.put("key", "value");    **

  • inflateTable(threshold); 初始化table及参数值--> 第一次put

  • roun dUpToPowerOf2 该方法为找到大于toSize的最小的2的幂数

  • threshold 初始化扩容的临界值,扩容时使用

  • table = new Entry[capacity]; 创建数组

  • putForNullKey(value);

  • 存放key值为null的键值对。首先判断是否存在key。如果存在,则替换,返回替换之前的值。不存在则存储在数组下标为0的位置,hash值为0。执行addEntry方法。

  • hash = hash(key); indexFor(hash, table.length); 获取key对应的数组的索引

  • for (Entry<K,V> e = table[i]; e != null; e = e.next) 判断是否存在key值,存在则替换Value,返回替换之前的Value值

  • addEntry(int hash, K key, V value, int bucketIndex)

  • 如果满足扩容条件(size >= threshold) && (null != table[bucketIndex]),则进行扩容

  • 之后添加到链表中(头插法)

  • Entry<K,V> e = table[bucketIndex]; 保存当前节点

  • table[bucketIndex] = new Entry<>(hash, key, value, e); 新建entry,next指向当前节点

put方法总结

  1. 判断是否是第一次执行put方法。如果是,初始化table以及threshold(扩容的临界值)。

  2. key==null,默认存储在数组索引为0的位置。判断是否存在key对应的value。存在则替换,返回替换前的value。不存在则判断是否满足扩容条件。之后使用头插法把key和value插入到链表

  3. key!=null,根据key得到hash值,然后 hash & table.length,得到存放数据的数组索引i。判断是否存在key对应的value。存在则替换,返回替换前的value。不存在则判断是否满足扩容条件。之后使用头插法把key和value插入到链表、

Resize方法源码

扩容时2 * table.length

  • 如果当前容量是MAXIMUM_CAPACITY,此方法不调整table的大小,但是将阈值设置为Integer.MAX_VALUE。 防止以后调用。

  • 否则新建entry,容量为当前容量的两倍。

  • void transfer(Entry[] newTable, boolean rehash)

  • 转移table数据到newTable。默认情况下不会进行rehash。重新计算entry对应的数组的索引。扩容时也是头插法

Resize方法总结

  1. 如果当前容量是MAXIMUM_CAPACITY,此方法不调整table的大小,但是将阈值设置为Integer.MAX_VALUE。 防止以后调用。
  2. 否则新建entry,容量为当前容量的两倍。
  3. 转移table数据到newTable。默认情况下不会进行rehash。重新计算entry对应的数组的索引
  4. 重新计算threshold(扩容的临界值)

JDK8

初始化HashMap

Map<String, String> map = new HashMap();
  • tableSizeFor(cap) 查找大于cap的最小的2倍幂

    static final int tableSizeFor(int cap) {    //18
        // 容量减1 为了防止传递的cap刚好为2倍幂
        int n = cap - 1;    // 17 -> 00010001
        n |= n >>> 1;       //  00011001
        n |= n >>> 2;       //  00011111
        n |= n >>> 4;       //  00011111
        n |= n >>> 8;       //  00011111
        n |= n >>> 16;       //  00011111
        // 执行以上代码是为了让cap二进制的最高位到最末位都为1
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    

put方法源码

public V put(K key, V value) {    return putVal(hash(key), key, value, false, true);}
static final int hash(Object key) {    
    int h;    
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • hashMap重写的hash函数。hashMap可以存储null,并且固定存储在数组的0下标

目前对红黑树理解不足,无法描述其执行流程。以后补充。

@6:treeifyBin方法内部会判断当前table长度是否小于 MIN_TREEIFY_CAPACITY(节点被树化时的最小的hash表容量)。小于的话就执行resize()方法。否则转为红黑树

put方法总结

  1. 判断是否是第一次执行put方法。如果是,扩容。

  2. 计算key1的hash值对应的数组下标i。

  3. table[i]为null,直接新建节点指向当前table位置,然后查看是否满足扩容条件,满足就扩容,不满足返回null。

  4. table[i]不为null。判断table[i]节点与传入的key是否一致。

  5. 一致,记录当前节点value为oldValue,然后覆盖当前节点的value值,返回oldValue

  6. 不一致。如果当前节点类型为TreeNode。如果有对应的key,返回节点。没有返回null。否则遍历链表,如果出现key一致,记录节点返回。没有遍历到最后,根据key,value创建node,添加到尾部。

  7. 对返回的节点判断。不为null,覆盖value值,返回oldValue

Resize()扩容源码

@6: 扩容后长度为原来table得到2倍。于是通过**(e.hash & oldCap == 0)**把newTab分为两部分,高位和低位。原来链表的键值对,一般放在高位,一般在低位

**e.hash & oldCap == 0      **oldCap = 16,二进制为10000,第5位为1。e.hash & oldCap 是否等于0就取决于e.hash的第5 位是0还是1,这就相当于有50%的概率放在新hash表低位,50%的概率放在新hash表高位。

Resize()方法总结

  1. oldTab为null时,需要初始化table,使用默认值计算table的容量和扩容的临界值,创建newTable并返回
  2. oldTab不为null时,判断容量是否是最大值,是就让扩容的临界值等于最大值,防止以后调用。不是就扩容为原来容量的2倍。并计算出扩容的临界值。创建newTable,遍历oldTable,转移数据并返回newTable