HashMap相关问题及解析

前言
    看了一些HashMap的相关视频和学习资料,一直在搞项目,这个知识点搁置有一段时间了,学习嘛,就是要自己动手做,动手写
    今天就脑子里的东西来回忆一下,一起记忆一下知识点以及针对HashMap的常见问题
    这就不解析源码了,如果喜欢看源码的就等着啥时候有时间了,在做一个纯代码的解读
 
先来看看图
 
 
备注
  1. 节点分为红色或者黑色;
  2. 根节点必为黑色;
  3. 叶子节点都为黑色,且为null;
  4. 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
  5. 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
  6. 新加入到红黑树的节点为红色节点;
     红黑树是均衡二叉树,需要具备自动维持平衡的性质,上面的6条就是红黑树给出的自动维持平衡所需要具备的规则
 
夺命n连问
 
   为什么HashMap数组要有默认长度?长度为多少? 为什么是16 ?
 
   为什么书写格式为1<<4,而不是直接写16 ?
 
   它的上限是多少?为什么是2的n次幂而不是其他?
 
解析:
 
Jdk1.8中,在调用HashMap的构造函数定义HashMap的时候,就会进行容量的设定。而在Jdk 1.7中,要等到第一次put操作时才进行这一操作, 默认情况下,当我们设置HashMap的初始化容量时,实际上HashMap会采用第一个大于该数值的2的幂作为初始化容量,所以说HashMap并不一定会直接采用我们传入的数值,而是经过计算,得到一个新值,目的是提高hash的效率
如果没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容,而HashMap中的扩容机制决定了每次扩容都需要重建hash表,非常影响性能。 所以为了提升效率,设置默认长度是很有必要的
 
HashMap中Capacity的值大小为16-------书写:1<<4;
 
默认值是16,是出于以下几点考虑的:
                                                    减少hash碰撞
 
                                                    提高map查询效率
 
                                                    分配过小防止频繁扩容
 
                                                    分配过大浪费资源
在put的过程中,会根据key调用hashcode()方法,计算出相应的Hash值,然后在将得到的int值对 数组长度 进行取模,然而为了考虑性能,Java总采用按位与操作实现取模操作,为了保证能够均匀的使用到每一个位置那么取模后index值的范围必须为0~(2^n)-1,所以当求index的时候必须保证数组长度为2的n次幂,而与Hash值做位运算的值是数组长度-1,保证了其为奇数,从而确保index的范围在 0~(2^n)-1之间,因此保证了能够均匀的使用到每一个位置;如果不能保证为奇数,通过计算得出一个结论,就是在 0~(2^n)-1范围内永远是有一个数无法通过取模运算获得,从而在数组中该值的下标位就会使用不到,就会增加其他位置的Hash碰撞概率,造成其他index的索引位的数据结构复杂化,而影响整个map的查询效率
 
另外, 数组长度为2的n次幂的时候,不同的key算得的index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高
 
HashMap中Capacity的Max值为1073741824 -------书写:1<<30
 
 
接下来就是负载因子
 
什么是负载因子?
         负载因子是和扩容机制有关的,意思是如果当前容器的容量,达到了我们设定的阀值,就要开始执行扩容操作
比如说当前的容器容量是16,负载因子是0.75,16*0.75=12,也就是说,当容量达到了12的时候就会进行扩容操作
理解了负载因子的意思之后我想为什么需要负载因子就不需要解释了
为什么负载因子是0.75,而不是0.5或1?
    分析:
       HashMap只是一个数据结构,既然是数据结构最主要考虑的就是节省时间和空间,那么怎么做到节省呢?
 
      负载因子1.0 
       负载因子是1.0的时,也就意味着,只有当数组的16个位置全部填充满了,才会发生扩容。
      这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率
 
      负载因子0.5
       负载因子是0.5的时,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低,查询效率就会增加。 但是,这时候空间利用率就会大大的降低,原本存储10M的数据,现在就需要20M的空间
 
总结
 
负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率
 
为什么链表的长度为8时转换为红黑树?为什么为6时又转换链表?
 
        当hashCode离散性很好的时候,树形结构用到的概率非常小,因为数据均匀分布在每个链表中,几乎不会有某链表的长度会达到阈值
        在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布
        不过理想情况下随机hashCode算法中所有链表中节点的分布频率会遵循泊松分布,也就是链表长度达到8个元素的概率为0.00000006,几乎是不可能事件
 
        所以选择8为链表转换为红黑树的阀值绝非是心血来潮
 
         因为操作红黑树时会涉及到左旋,右旋等操作,而单链表不需要,所以当需要对节点进行操作时,红黑树的成本要高很多,所以为了减小操作成本,当节点数较小时将红黑树转换为链表,再进行操作是较好的选择
 
HashMap的resize
 
    一、扩容
 
         当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容
        简单的来说:
        HashMap中的元素个数超过数组大小*loadFactor(默认情况下为0.75)时,就会进行数组扩容。
        也就是, 默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后通过rehash重新计算每个元素在数组中的位置( rehash之后数组元素的位置要么在原位置,要么在原位置再移动2次幂的位置 ),并修改阀值
    
备注:
 
      Jdk1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,JDK1.8则不会倒置
 
     Jdk1.8对扩充的优化:
            查看原hash值新增的bit是1还是0,是0索引没变,是1索引变成“原索引+oldCap”,从而省去了重新计算hash值的时间,同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket
 
   二、初始化
        
       HashMap会根据 初始容量和负载因子来初始化,当初始容量小于最大条目数除以负载因子时,则会发生 rehash 操作。
       rehash操作即重建内部数据结构,一般是增加数组长度为原来的两倍,rehash过程中会重新计算每个元素在数组中的位置,是一个非常消耗性能的操作。所以程序设计时,如果我们已经预知HashMap中元素的个数,那么预设元素的个数是可以有效的提高HashMap性能的
 
HashMap还有很多值得研究的东西这里就不一一赘述了,喜欢的同学多去看看源码慢慢分析和使用就好了,共勉!加油!