加载因子:loadFactor
- 默认0.75,为扩容使用;
- 初始化 或 扩容方法resize(),初始化数组长度默认16(2的幂次方);
阈值:threshold:
整个hashMap的元素个数(size属性)超过此值就会扩容;
每次都是双倍扩容;
扩容第二个地方:其中一个链表长度>=8时,会去优化单向链表,数组长度小于64,扩容,大于等于,当前下标的所有node节点转换为双向链表+红黑树;
修改次数modCount
数据对象数组:table,Node
hashCode是int类型,一个int是4个字节,一个字节8bit位,初始化的长度16,计算下标是15,所以低4位一直就是1,高28bit位是0
Map<String, Object> map = new HashMap<>(len);
初始化:不传默认16;传入的len如果不是2的幂次方,那么会处理成大于len的2的幂次方数值 tableSizeFor(len)
生成的map对象是放在jvm的堆中,图中圈出来的属于线程栈的对象,因为方法内需要多次使用到,所以如果每次都从堆中获取table会有性能上的消耗,直接从线程栈中取值会更快
计算K的hash值:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
h右移16位,让hashCode的高位也参与到下标的计算
由K的hash值和数组长度计算Node节点的下标,
if ((p = tab[i = (n - 1) & hash]) == null) // 该下标元素为null,那么把新的node放到该下标 tab[i] = newNode(hash, key, value, null);
如果不为空,就拼接到该下标的链表尾部(jdk1.7是头插法),如果该链表长度 >= 8,那么就会对map的数据进行处理,避免单向链表检索数据性能慢;O(n);这里实际判断的时候链表长度是9,因为p也是一个节点,然后又拼接了一个next,所以长度是9
在转红黑树之前会判断数组的长度是否大于64,如果小于64,那么会对数组进行扩容,均摊到其他下标中,保证链表长度小于8;如果链表长度大于等于8,并且map的数组长度大于64了,那么就把当前下标的Node单向链表转换成 双向链表+红黑树 结构 TreeNode,复杂度O(log n)。
扩容:
扩容时是双倍扩容,也就是原容量左移一位,
对单链表拆分:会判断高位或者低位,高位链表下标=原下标+原数组大小;
对TreeNode拆分:如果拆分也是区分高位或者低位,拆分的链表长度 <= 6,那么就会又变为单向链表;剩余Node需要重新生成一个树结构(低位判断是否有高位链表,有,说明原来的树结构有拆分,反之亦然)
有了数组,为什么还要使用hashmap?
首先,hashmap是数组+链表的结构,数组可以快速定位,链表可以提高写入速度,结合了数组和链表的优点。
java7:使用头插法:速度是最快的
java8:尾插法,红黑树
总结:
链表用头插法是最快的
数组:查询快,插入慢
定位速度快,可以快速插入,插入之后需要后移
链表:查询慢,插入快,定位速度慢,插入后不需要移动