JDK8-HashMap底层源码解析(一)

986 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

new

JDK1.7:数组+链表
JDK1.8:hash表=数组+链表+红黑树
哇瑟,一个比一个背的熟,面试时稍微问一下就懵逼。

HashMap提供了4个构造函数:

  • 无参构造新建时会给负载因子设置默认为0.75,容量大小默认为16,阈值为0;

    • 这里容量大小默认16只是我们约定的而已,没有添加元素前我们底层的Node数组的length一直为0,一切操作都在put操作之后才展开。
  • 有参构造,使用参数提供的负载因子,容量会由tableSizeFor方法计算出大于等于参数值的一个二次方的数值;阈值初始值会使用容量的值,然后在put元素的时候才会使用公式:容量*负载因子 重新赋值;

tableSizeFor方法具体实现:

我们可以通过反射来查看HashMap在初始化和put后容量阈值的变化,代码如下:

    private  static void test() throws Exception {
        //指定初始容量15来创建一个HashMap
        HashMap m = new HashMap(16);
        //获取HashMap整个类
        Class<?> mapType = m.getClass();
        //获取指定属性,也可以调用getDeclaredFields()方法获取属性数组
        Field threshold =  mapType.getDeclaredField("threshold");
        //将目标属性设置为可以访问
        threshold.setAccessible(true);
        //获取指定方法,因为HashMap没有容量这个属性,但是capacity方法会返回容量值
        Method capacity = mapType.getDeclaredMethod("capacity");
        //设置目标方法为可访问
        capacity.setAccessible(true);
        //打印刚初始化的HashMap的容量、阈值和元素数量
        System.out.println("初始数据 - 容量:"+capacity.invoke(m)+"    阈值:"+threshold.get(m)+"    元素数量:"+m.size());
        for (int i = 0;i<25;i++){
            m.put(i,i);
            //动态监测HashMap的容量、阈值和元素数量
            System.out.println("容量:"+capacity.invoke(m)+"    阈值:"+threshold.get(m)+"    元素数量:"+m.size());
        }
    }

输出结果如下:

put

接下来我们看put方法:并没有直接使用key的hashcode方法来生成哈希值,而是执行了这个操作: (h = key.hashCode()) ^ (h >>> 16)

在执行hashcode方法后再异或 这个右移16位的值,得到了一个新的哈希值,但是哈希冲突仍然不能避免。

接着看putVal方法:

  1. 如果node数组为空,即table为空,会执行resize方法返回一个Node<K,V>[] 赋值给tab变量,也就是在这个方法中会给阈值重新计算

    • resize()方法主要是用来扩容的,下面链表尾插入的时候也会用到;
  2. 如果tab对应下标没有值,则新建一个Node放入数组;

    • 这里的(p = tab[i = (n - 1) & hash]) 就体现出我们要求数组长度是2的幂次方的重要性,只有n为2的幂次方,n-1 和hash值与运算才能得到0到n-1的下标值。
  3. 如果对应下标有值,hash相同key相同,则会在后面根据一个boolean值判断只够进行覆盖;

    • 为什么会有boolean值,put操作默认为true会执行进行覆盖,但还有一个方法map.putIfAbsent(),也是调用的putVal方法;
    • 另外一点就是put方法和putIfAbsent方法都是有返回值的,返回的都是执行插入操作前key对应的value值。
  4. 如果Node是TreeNode类型,即Node数组对应下标中是一个红黑树,将要操作红黑树插入;

  5. 否则就是链表的尾插入,即Node数组对应下标中是一个链表;

    • JDK1.7是头插入 ,1.8是尾插,使用头插会改变链表上的顺序,采用尾部插入能保持链表原本的顺序,jdk1.7的同步插入在扩容时会造成错误,链表中的指向会发生错乱出现循环引用 。
    • 链表插入会循环判断node.next是否为null,链表长度大于8时转成红黑树,因为先判断在插入所有链表会有9个元素,会调用treeifyBin().

treeifyBin方法会先判断Node数组是否大于64,小于会调用resize()扩容 一个长链表转为两个短链表;

大于64则会使用do while循环将所有的Node改造为TreeNode,并且变为一个双向链表,给节点的prev和next赋值;

最后通过treeify将链表转为红黑树。