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方法:
如果node数组为空,即table为空,会执行resize方法返回一个Node<K,V>[] 赋值给tab变量,也就是在这个方法中会给阈值重新计算
;
resize()方法主要是用来扩容的,下面链表尾插入的时候也会用到;
如果tab对应下标没有值,则新建一个Node放入数组;
这里的(p = tab[i = (n - 1) & hash]) 就体现出我们要求数组长度是2的幂次方的重要性,只有n为2的幂次方,n-1 和hash值与运算才能得到0到n-1的下标值。
如果对应下标有值,hash相同key相同,则会在后面根据一个boolean值判断只够进行覆盖;
为什么会有boolean值,put操作默认为true会执行进行覆盖,但还有一个方法map.putIfAbsent(),也是调用的putVal方法;
另外一点就是put方法和putIfAbsent方法都是有返回值的,返回的都是执行插入操作前key对应的value值。
如果Node是TreeNode类型,即Node数组对应下标中是一个红黑树,将要操作红黑树插入;
否则就是链表的尾插入,即Node数组对应下标中是一个链表;
JDK1.7是头插入 ,1.8是尾插,使用头插会改变链表上的顺序,采用尾部插入能保持链表原本的顺序,jdk1.7的同步插入在扩容时会造成错误,链表中的指向会发生错乱出现循环引用 。
链表插入会循环判断node.next是否为null,链表长度大于8时转成红黑树,因为先判断在插入所有链表会有9个元素,会调用treeifyBin().
treeifyBin方法会先判断Node数组是否大于64,只有大于64才会转为红黑树,不然会调用resize()扩容 一个长链表转为两个短链表
原文:https://juejin.cn/post/7103785290619158536