HashMap

HashMap

        实现了 Map<K,V> 接口, HashTable实现了Dictionary<K,V>

        数据结构 : 数组 + 链表 + 红黑树(增加查询速度)

 

        基本使用方法 :

 

 

     1 . 从测试用例开始查看HashMap的源码

         当然在查看前 , 我们先来认识几个我们需要知道的成员变量.

         static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   // 默认初始化可存放元素个数

         static final int MAXIMUM_CAPACITY = 1 << 30;    // 数组内最大存放元素个数

         static final float DEFAULT_LOAD_FACTOR = 0.75f;   //负载因子

         static final int UNTREEIFY_THRESHOLD = 6;    //当链表少于6个的时候, 会再次转为链表

         static final int MIN_TREEIFY_CAPACITY = 64;  // 当一个链表个数达到8,会尝试将其变为红黑树,但是如果这个时候map内元素的总个数 < 64 , 会优先考虑扩容.

   transient Node<K,V>[] table;      // 操作的数组

 

 

 

 

 

 

 

 

 

 

 

 

 a . 直接开始看第三个构造方法.

      1. 首先校验初始化元素个数是否 < 0 ,  true 的话 , 就抛出异常.

      2. 初始化的元素个数是否已经超出了最大值, 如果超出,按最大值来算.

      3. 负载因子是否为 < = 0 或者 不是个Float类型的数字. ( NaN = Not a Number ) , 为true ,抛出异常

      4. 在为 threshold 这个元素赋值的时候, 需要对这个元素重新计算. 要求必须为2的倍数

          当为2的倍数的时候,  key的hash值 % 数组长度 = key的hash值 & (n-1)  . n是数组长度 .

          包括后面扩容时,高位链与低位链的使用.

     

 

 

  2. 接下来开始看 put 方法.

        

 

      

        a . 直接调用了 putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) 方法. 我们先来看一下这个方法的4个参数

            1. hash  . 是将key进行hash运算后的结果. 这里我们可以看到 HashMap的 key 和 value都是允许为空的,如果key为null ,默认存储在数组的第一位.

           

 

 

             2. 这里补充一个点,当数组的Key为一个对象的时候,因为在我们实际的场景中,希望只要对象内属性一样,就默认为同一个.所以我们就需要重写这个对象的 hashCode()方法了.

             3. 这里的key进行hash()计算,不仅仅是进行了一个hashCode(), 因为是int类型, 这个还将计算出来的结果像右移16位,让高位和地位进行了异或,使高位和地位都参与了运算,在位桶内分布更加均匀.

     b. 第二个参数就是value,我们传输的值

     c . Boolean onlyIfAbsent = false .当这个元素为false的时候,默认key相同时,会对value进行覆盖.为true,则保持第一次的value值不变

     d . evict if false, the table is in creation mode. 默认为true

 

   2. 开始看 putVal方法.

 

            源码如下 :

 

  

 

 

 

 

 

 

 

a .  首先判断成员变量 Table 是否为Null ,或者当前数组长度为0 , 那么就需要进行扩容(其实这个时候叫初始化比较合适.)

      进入resize()方法

      初始化进入的时候(见下图代码),

     1.  首先将 成员变量数组 table赋值给 oldTab (当前未扩容的数组)

     2.  oldCap = oldTab.length   (初始化时为 0) 未扩容前数组的长度. 

     3.  threshold , 当我们传输数组初始化长度的时候,这里会将其变为2的幂次方. 比如我们传输的是10 , 那么这里就是16. 如果我们不传输 ,这里就是0 (int类型的默认值) .

     4 . 目前table还是null , 第一个if(old>cap) ,此方法暂时不会进入.

     5. else if ( oldThre > 0) , 如果我们传输的默认大小 10 (变为16了) , 那么现在 newCap = 16

     6 . 此时newThr 还是默认值 0 , 进入 if ( newThr == 0 ) , float ft = 16 * 0.75  , 然后重新赋值给 threshold .

     7. 同时table= new Node[16] ;

     8. 这个时候 table就初始化好了. 后面暂时不用看,因为我们的 oldTable == null

    

 

 

 

   a. 接下来原始流程继续往下看.

 

 

  a . 如上图, 1 的值为当前key所在位桶的索引

  b. 2的值为table在这个索引上存储的头结点Node .

  c. 如果头结点都为Null ,说明当前位桶无元素,直接当前位桶新增一个Node. Node的值为null .  tab[i] = newNode(hash, key, value, null);  

      顺便这里描述一下Node. 这是存在于HashMap的一个静态内部类, 有hash , key , value , next 属性.

 d. 继续往下看,如果头结点不为空.

 

 

 

 进入到 else 中.

 

 

 

 

 a . 如果hash值一样(与hashcode()方法有关系), 并且 (k == p.key 或者 key.equals(K) , 这里又用到了equals方法) , 所以当HashMap的key是对象的时候, 我们需要重写这个对象的 hashcode() 方法 和 equasl()方法.

 b . 如果此位桶的头结点 instanceof TreeNode (红黑树类型) , 那就采用 putTreeVal 方法加入到红黑树内.

 c . 接下来进入最后一个else.

 

 

 a . 说明此位桶上, 链表不为空  且长度不超过 8 , 那就需要开始for循环这个链表覆盖或者新增元素Node了.

 b .  先看for循环内的第一个if . 如果当前阶段的next为null,直接加入new Node() 加入链表的尾端 (多线程情况下,无锁控制,容易出现覆盖Node被覆盖.尾插法) , 如果插入之前的长度 >=7 , 那么这时候就需要尝试将链表改为红黑数了.结束后,跳出for循环.

 c .  第二个if, 如果链表中存在一个元素Key相同,将这个元素赋值给key,跳出for循环.

 d . 继续

 

 

 a . 如果e != Null , 说明存在重复的元素.

  这个时候就说到我们之前说的 onlyIfAbsent , 现在默认是 false , 会进行覆盖. 并将oldValue返回. 因为是覆盖,所以不需要 size++ .

 b . 如果为空,说明就是新增了一个元素, 成员变量 size++ .(所以说是非线程安全的)

 c .如果增加之后的size > 临界值 threshold (这个时候就是 16*0.75 = 12) , 这个时候就是真正的扩容了.

 d. 扩容结束,返回null.

 e . 接下来看一下 从 16 扩充到32 的时候,是如何扩容的 (新的数组 位桶为32 , 最大存储容量 = 32 *0.75 = 24) . 再次查看resize方法.

 

 

 

 

 

 

 

 

 

   

 

posted @ 2020-05-09 11:45  java_小跟班  阅读(198)  评论(0)    收藏  举报