ENTRY组成
1.Map数组【put】值
数组如果想要放一个数值, 或者获取一个值,必须根据数组【i】的下标:
当hash.put("1","2")时,决定这个Entry对象位置的是这个 key ,然后根据这个 key 经过 hash 计算产生一个下标 index :关系暂时理解如下图所示
那么当map中第一次放值后,第二个值相同的index值,怎么放?
JDK1.7头插法
会为第一个值,增加一个属性 next 指针的属性,设置下一个节点下标
,其中这个next 代表的是Tablei[ndex],当第二个值put时,会在把这个entry对象的next指向第一个entry;
Map初始化数组
tableSizeFor的功能(不考虑大于最大容量的情况)是返回大于输入参数且最近的2的整数次幂的数。
map构造方法初始化容量是16;但是如果 new HashMap(10)时,其实背后默认还是16,但是为什么要这么做?
- 图中第一句,初始化数组,通过第一句2的幂指数函数计算后,更改为16;
- 第三句就是拿着这个数值给map初始化;
- 第二句其实扩容机制的计算(数组的容量*加载因子)【扩容的域值】;
- 第四局就是一个hash种子;
问题1:为什么初始化数组时,它的容量一定要是2的幂指函 数?
问题2: 为什么在计算hashcode值,它要去计算右移和异或运算?
Java1.4版本以后map真实的的计算下标值方式:&
上面用取余来计算下标值,其实是JDK1.4的计算方式,但是效率太慢,所以更新之后实际上是通过 &(与) 操作;
问题1:为什么初始化数组时,它的容量一定要是2的幂指函 数?
如下图所示,
如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,
而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在于h与操作,
最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费 。
map 【put】 的源码
如图所示,
for循环就是检测,是否有相同的key值,如果有相同的key,就会返回出来,并且覆盖老值;
从图中的 if(e.hash == hash && ((k = e.key ) == key || key.equals(k))) 这句代码就可以看出来:如果hash地址相同,不代表就是一个对象;
2.map 扩容
- JDK1.7版本后,先扩容;
- JDK1.8版本后,先添加元素在扩容;
不断地put,链表越来越长,为了优化get的效率,只能进行扩容 (其实扩容就是重新生成一个数组) ;
然后原先的ENTRY对象是怎么放到新的数组?
原先是经过计算出来hashcode,然后再根据 hashcode & length -1 计算出来下标;
扩容时:不用计算hashcode 值,只是 hashcode & length -1 计算下标就可以;
如果hashcode值,不重新计算,会不会导致老entry对象还在原来的数组下标下?
其中包含一个规律:
这个16指的就是扩容之前的容量:这样的不会导致所有对象都会加上去么?待解决
3.map 【get】
1.先去判断是否为空;
2.然后根据key值经过hashcode计算,算出来下标;
3.拿着下标遍历这节点,先比较hashcode值, 再比较内容,最后获取出来这个值;
4.红黑树
红黑树有两个条件:【数组长度】、【链表长度】
因为不管怎么优化,数组的某个下标位置,也还是会出现链表过长的情况;
于是就采用红黑树进行优化;
那为什么用红黑树?而不用其他二叉树?
要考虑到get与set的效率,红黑树是一个折中的结构;
什么时候由链表转化为红黑树?什么时候红黑树转化为链表?
当链表的元素>=8,就会改成红黑树,这个8数值是一个开发程序员经验以及实践校验的结果:当>=8时,就会影响到get与set的效率。
当红黑树的元素<=6时,就会转化为链表;
为什么一个是8,一个是6?不能相同?
防止某个间隔时间内,你频繁的操作map,造成红黑树与链表反复的切换,这样也会影响性能;
为什么Java1.7版本原来用的是头插法,而在Java1.8后,换成了尾插法?
其实是因为转变红黑树的原因,既然要转变红黑树,那么就得判断这个链表长度,就得需要挨个计数,所以既然要遍历判断是否扩容,所以索性就在遍历过程中,把元素放在了尾部 -> 提高了效率,这是一个其中原因;