• 源码分析 HashMap 1.7


    1.0 类的定义

    public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable

    2.0 数据结构

      2.1 简要描述

        HashMap 采用的数据结构 = 数组 + 链表  该数据结构也被称为拉链法,具体描述如下:

          

      2.2 示意图

          

      2.3 简单存储流程

              

      2.4 数组&链表元素的实现类

        HashMap中的数组元素和链表节点采用Entry类实现

        Entry对象本质 = 1个映射(键 - 值对),属性包括:键(key)、值(value) & 下1节点(next) = 单链表的指针 = 也是一个Entry对象,用于解决hash冲突

      

    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            int hash;
        ...
    }

    3.0 使用方式

    public class Test{
         public static void main(String[] args) {
          Map<String, String> map = new HashMap<String, String>();
          map.put("1", "value1");
          map.put("2", "value2");
          map.put("3", "value3");
          
          //第一种:普遍使用,二次取值
          System.out.println("通过Map.keySet遍历key和value:");
          for (String key : map.keySet()) {
           System.out.println("key= "+ key + " and value= " + map.get(key));
          }
          
          //第二种
          System.out.println("通过Map.entrySet使用iterator遍历key和value:");
          Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
          while (it.hasNext()) {
           Map.Entry<String, String> entry = it.next();
           System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
          }
          
          //第三种:推荐,尤其是容量大时
          System.out.println("通过Map.entrySet遍历key和value");
          for (Map.Entry<String, String> entry : map.entrySet()) {
           System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
          }
        
          //第四种
          System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
          for (String v : map.values()) {
           System.out.println("value= " + v);
          }
         }
    }

    4.0 重要参数(变量)

      HashMap 中的重要参数 = 容量 、加载因子、扩容阈值

        容量(capacity):Hashmap中数组长度   必须是2的幂,且小于最大容量2的30次方

          默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16

            static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

          最大容量 = 2的30次方(若传入的容量过大,将被最大值替换) 

            static final int MAXIMUM_CAPACITY = 1 << 30;

         加载因子(Load factor)HashMap在其容量自动增加前可达到多满的一种尺度

           a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了) 

           b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)

          默认加载因子

            static final float DEFAULT_LOAD_FACTOR = 0.75f;

          实际加载因子

            final float loadFactor;

        扩容阈值(threshold)当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表

          a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数 

          b. 扩容阈值 = 容量 x 加载因子

            int threshold;

        其它

          数据存储的位置

            transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

          HashMap存储的最大数量

            transient int size;

    5.0 源码分析

      

      5.1构造函数分析

    /**
      * 函数使用原型
      */
      Map<String,Integer> map = new HashMap<String,Integer>();
    
     /**
       * 源码分析:主要是HashMap的构造函数 = 4个
       * 仅贴出关于HashMap构造函数的源码
       */
      public class HashMap<K,V>
          extends AbstractMap<K,V>
          implements Map<K,V>, Cloneable, Serializable{
    
        // 省略上节阐述的参数
        
      /**
         * 构造函数1:默认构造函数(无参)
         * 加载因子 & 容量 = 默认 = 0.75、16
         */
        public HashMap() {
            // 实际上是调用构造函数3:指定“容量大小”和“加载因子”的构造函数
            // 传入的指定容量 & 加载因子 = 默认
            this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 
        }
    
        /**
         * 构造函数2:指定“容量大小”的构造函数
         * 加载因子 = 默认 = 0.75 、容量 = 指定大小
         */
        public HashMap(int initialCapacity) {
            // 实际上是调用指定“容量大小”和“加载因子”的构造函数
            // 只是在传入的加载因子参数 = 默认加载因子
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
            
        }
    
        /**
         * 构造函数3:指定“容量大小”和“加载因子”的构造函数
         * 加载因子 & 容量 = 自己指定
         */
        public HashMap(int initialCapacity, float loadFactor) {
    
            // HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的 > 最大容量
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
    
            // 设置 加载因子
            this.loadFactor = loadFactor;
            // 设置 扩容阈值 = 初始容量
            // 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算,下面会详细讲解  
            threshold = initialCapacity;   
    
            init(); // 一个空方法用于未来的子对象扩展
        }
    
        /**
         * 构造函数4:包含“子Map”的构造函数
         * 即 构造出来的HashMap包含传入Map的映射关系
         * 加载因子 & 容量 = 默认
         */
    
        public HashMap(Map<? extends K, ? extends V> m) {
    
            // 设置容量大小 & 加载因子 = 默认
            this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                    DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    
            // 该方法用于初始化 数组 & 阈值,下面会详细说明
            inflateTable(threshold);
    
            // 将传入的子Map中的全部元素逐个添加到HashMap中
            putAllForCreate(m);
        }
    }
    1. 此处仅用于接收初始容量大小(capacity)、加载因子(Load factor),但仍无真正初始化哈希表,即初始化存储数组table
    2. 此处先给出结论:真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时。下面会详细说明

      5.2 向HashMap中添加数据

     /**
       * 函数使用原型
       */
       map.put("Android", 1);
            map.put("Java", 2);
            map.put("iOS", 3);
            map.put("数据挖掘", 4);
            map.put("产品经理", 5);
    
       /**
         * 源码分析:主要分析: HashMap的put函数
         */
        public V put(K key, V value)
    (分析1)// 1. 若 哈希表未初始化(即 table为空) 
            // 则使用 构造函数时设置的阈值(即初始容量) 初始化 数组table  
            if (table == EMPTY_TABLE) { 
            inflateTable(threshold); 
        }  
            // 2. 判断key是否为空值null
    (分析2)// 2.1 若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0]
            // (本质:key = Null时,hash值 = 0,故存放到table[0]中)
            // 该位置永远只有1个value,新传进来的value会覆盖旧的value
            if (key == null)
                return putForNullKey(value);
    
    (分析3) // 2.2 若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)
            // a. 根据键值key计算hash值
            int hash = hash(key);
            // b. 根据hash值 最终获得 key对应存放的数组Table中位置
            int i = indexFor(hash, table.length);
    
            // 3. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
    (分析4)// 3.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue; //并返回旧的value
                }
            }
    
            modCount++;
    
    (分析5)// 3.2 若 该key不存在,则将“key-value”添加到table中
            addEntry(hash, key, value, i);
            return null;
        }

      根据上述源码分析可以画出以下流程图

      

      下面对五个分析点进行详细分析

        分析一:初始化哈希表

     /**
         * 函数使用原型
         */
          if (table == EMPTY_TABLE) { 
            inflateTable(threshold); 
        }  
    
       /**
         * 源码分析:inflateTable(threshold); 
         */
         private void inflateTable(int toSize) {  
        
        // 1. 将传入的容量大小转化为:>传入容量大小的最小的2的次幂
        // 即如果传入的是容量大小是19,那么转化后,初始化容量大小为32(即2的5次幂)
        int capacity = roundUpToPowerOf2(toSize);->>分析1   
    
        // 2. 重新计算阈值 threshold = 容量 * 加载因子  
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  
    
        // 3. 使用计算后的初始容量(已经是2的次幂) 初始化数组table(作为数组长度)
        // 即 哈希表的容量大小 = 数组大小(长度)
        table = new Entry[capacity]; //用该容量初始化table  
    
        initHashSeedAsNeeded(capacity);  
    }  
    
        /**
         * 分析1:roundUpToPowerOf2(toSize)
         * 作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
         * 特别注意:容量大小必须为2的幂,该原因在下面的讲解会详细分析
         */
    
         private static int roundUpToPowerOf2(int number) {  
       
           //若 容量超过了最大值,初始化容量设置为最大值 ;否则,设置为:>传入容量大小的最小的2的次幂
           return number >= MAXIMUM_CAPACITY  ? 
                MAXIMUM_CAPACITY  : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;  

      分析二:当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]

       

       /**
         * 函数使用原型
         */
          if (key == null)
               return putForNullKey(value);
    
       /**
         * 源码分析:putForNullKey(value)
         */
          private V putForNullKey(V value) {  
            // 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
            // 1. 若有:则用新value 替换 旧value;同时返回旧的value值
            for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
              if (e.key == null) {   
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        modCount++;  
    
        // 2 .若无key==null的键,那么调用addEntry(),将空键 & 对应的值封装到Entry中,并放到table[0]中
        addEntry(0, null, value, 0); 
        // 注:
        // a. addEntry()的第1个参数 = hash值 = 传入0
        // b. 即 说明:当key = null时,也有hash值 = 0,所以HashMap的key 可为null
        // c. 对比HashTable,由于HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
        // d. 此处只需知道是将 key-value 添加到HashMap中即可,关于addEntry()的源码分析将等到下面再详细说明,
        return null;  
    
    }  

      分析三:计算存放数组 table 中的位置(即 数组下标 or 索引)

      

    /**
         * 函数使用原型
         * 主要分为2步:计算hash值、根据hash值再计算得出最后数组位置
         */
            // a. 根据键值key计算hash值 ->> 分析1
            int hash = hash(key);
            // b. 根据hash值 最终获得 key对应存放的数组Table中位置 ->> 分析2
            int i = indexFor(hash, table.length);
    
       /**
         * 源码分析1:hash(key)
         * 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)
         * JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
         * JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
         */
    
         // JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作  = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
         static final int hash(int h) {
            h ^= k.hashCode(); 
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
         }
    
          // JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
          // 1. 取hashCode值: h = key.hashCode() 
         //  2. 高位参与低位的运算:h ^ (h >>> 16)  
          static final int hash(Object key) {
               int h;
                return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
                // a. 当key = null时,hash值 = 0,所以HashMap的key 可为null      
                // 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
                // b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
         }
    
       /**
         * 函数源码分析2:indexFor(hash, table.length)
         * JDK 1.8中实际上无该函数,但原理相同,即具备类似作用的函数
         */
          static int indexFor(int h, int length) {  
              return h & (length-1); 
              // 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
    }

      可能存在的疑惑:

        为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?

      

      为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?

       

      为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?

        

      分析4:若对应的key已存在,则 使用 新value 替换 旧value

      /**
         * 函数使用原型
         */
    // 2. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                // 2.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue; //并返回旧的value
                }
            }
    
            modCount++;
    
            // 2.2 若 该key不存在,则将“key-value”添加到table中
            addEntry(hash, key, value, i);
            return null;

       替换流程如下:

      

      分析五:若对应的key不存在,则将该“key-value”添加到数组table的对应位置中(头插法&扩容)

       // 2.2 若 该key对应的值不存在,则将“key-value”添加到table中
            addEntry(hash, key, value, i);
    
       /**
         * 源码分析:addEntry(hash, key, value, i)
         * 作用:添加键值对(Entry )到 HashMap中
         */
          void addEntry(int hash, K key, V value, int bucketIndex) {  
              // 参数3 = 插入数组table的索引位置 = 数组下标
              
              // 1. 插入前,先判断容量是否足够
              // 1.1 若不足够,则进行扩容(2倍)、重新计算Hash值、重新计算存储数组下标
              if ((size >= threshold) && (null != table[bucketIndex])) {  
                resize(2 * table.length); // a. 扩容2倍  --> 分析1
                hash = (null != key) ? hash(key) : 0;  // b. 重新计算该Key对应的hash值
                bucketIndex = indexFor(hash, table.length);  // c. 重新计算该Key对应的hash值的存储数组下标位置
            }  
    
          // 1.2 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中--> 分析2
          createEntry(hash, key, value, bucketIndex);  
        }  
    
     /**
       * 分析1:resize(2 * table.length)
       * 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
       */ 
      void resize(int newCapacity) {  
        
        // 1. 保存旧数组(old table) 
        Entry[] oldTable = table;  
    
        // 2. 保存旧容量(old capacity ),即数组长度
        int oldCapacity = oldTable.length; 
    
        // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出    
        if (oldCapacity == MAXIMUM_CAPACITY) {  
            threshold = Integer.MAX_VALUE;  
            return;  
        }  
      
        // 4. 根据新容量(2倍容量)新建1个数组,即新table  
        Entry[] newTable = new Entry[newCapacity];  
    
        // 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 
        transfer(newTable); 
    
        // 6. 新数组table引用到HashMap的table属性上
        table = newTable;  
    
        // 7. 重新设置阈值  
        threshold = (int)(newCapacity * loadFactor); 
    } 
    
     /**
       * 分析1.1:transfer(newTable); 
       * 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
       * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
       */ 
    void transfer(Entry[] newTable) {
          // 1. src引用了旧数组
          Entry[] src = table; 
    
          // 2. 获取新数组的大小 = 获取新容量大小                 
          int newCapacity = newTable.length;
    
          // 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
          for (int j = 0; j < src.length; j++) { 
                // 3.1 取得旧数组的每个元素  
              Entry<K,V> e = src[j];           
              if (e != null) {
                  // 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
                  src[j] = null; 
    
                  do { 
                      // 3.3 遍历 以该数组元素为首 的链表
                      // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
                      Entry<K,V> next = e.next; 
                     // 3.4 重新计算每个元素的存储位置
                     int i = indexFor(e.hash, newCapacity); 
                     // 3.5 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
                     // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
                     e.next = newTable[i]; 
                     newTable[i] = e;  
                     // 3.6 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
                     e = next;             
                 } while (e != null);
                 // 如此不断循环,直到遍历完数组上的所有数据元素
             }
         }
     }
    
     /**
       * 分析2:createEntry(hash, key, value, bucketIndex);  
       * 作用: 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
       */  
    void createEntry(int hash, K key, V value, int bucketIndex) { 
    
        // 1. 把table中该位置原来的Entry保存  
        Entry<K,V> e = table[bucketIndex];
    
        // 2. 在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而形成链表
        // 即 在插入元素时,是在链表头插入的,table中的每个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即 解决Hash冲突)
        table[bucketIndex] = new Entry<>(hash, key, value, e);  
    
        // 3. 哈希表的键值对数量计数增加
        size++;  
    }

      链表的头插法如下如所示

       

       链表的扩容转移流程如下图所示

      

       5.3 从hashMap中获取数据

          

      源码分析如下所示

      

    /**
       * 函数原型
       * 作用:根据键key,向HashMap获取对应的值
       */ 
       map.get(key);
    
    
     /**
       * 源码分析
       */ 
       public V get(Object key) {  
    
        // 1. 当key == null时,则到 以哈希表数组中的第1个元素(即table[0])为头结点的链表去寻找对应 key == null的键
        if (key == null)  
            return getForNullKey(); --> 分析1
    
        // 2. 当key ≠ null时,去获得对应值 -->分析2
        Entry<K,V> entry = getEntry(key);
      
        return null == entry ? null : entry.getValue();  
    }  
    
    
     /**
       * 分析1:getForNullKey()
       * 作用:当key == null时,则到 以哈希表数组中的第1个元素(即table[0])为头结点的链表去寻找对应 key == null的键
       */ 
    private V getForNullKey() {  
    
        if (size == 0) {  
            return null;  
        }  
    
        // 遍历以table[0]为头结点的链表,寻找 key==null 对应的值
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
    
            // 从table[0]中取key==null的value值 
            if (e.key == null)  
                return e.value; 
        }  
        return null;  
    }  
     
     /**
       * 分析2:getEntry(key)
       * 作用:当key ≠ null时,去获得对应值
       */  
    final Entry<K,V> getEntry(Object key) {  
    
        if (size == 0) {  
            return null;  
        }  
    
        // 1. 根据key值,通过hash()计算出对应的hash值
        int hash = (key == null) ? 0 : hash(key);  
    
        // 2. 根据hash值计算出对应的数组下标
        // 3. 遍历 以该数组下标的数组元素为头结点的链表所有节点,寻找该key对应的值
        for (Entry<K,V> e = table[indexFor(hash, table.length)];  e != null;  e = e.next) {  
    
            Object k;  
            // 若 hash值 & key 相等,则证明该Entry = 我们要的键值对
            // 通过equals()判断key是否相等
            if (e.hash == hash &&  
                ((k = e.key) == key || (key != null && key.equals(k))))  
                return e;  
        }  
        return null;  
    }

      5.4其它api分析

    /**
       * 函数:isEmpty()
       * 作用:判断HashMap是否为空,即无键值对;size == 0时 表示为 空 
       */
    
    public boolean isEmpty() {  
        return size == 0;  
    } 
    
     /**
       * 函数:size()
       * 作用:返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
       */
    
       public int size() {  
        return size;  
    }  
    
     /**
       * 函数:clear()
       * 作用:清空哈希表,即删除所有键值对
       * 原理:将数组table中存储的Entry全部置为null、size置为0
       */ 
    public void clear() {  
        modCount++;  
        Arrays.fill(table, null);
        size = 0;
    }  
    
    /**
       * 函数:putAll(Map<? extends K, ? extends V> m)
       * 作用:将指定Map中的键值对 复制到 此Map中
       * 原理:类似Put函数
       */ 
    
        public void putAll(Map<? extends K, ? extends V> m) {  
        // 1. 统计需复制多少个键值对  
        int numKeysToBeAdded = m.size();  
        if (numKeysToBeAdded == 0)  
            return; 
    
        // 2. 若table还没初始化,先用刚刚统计的复制数去初始化table  
        if (table == EMPTY_TABLE) {  
            inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));  
        }  
      
        // 3. 若需复制的数目 > 阈值,则需先扩容 
        if (numKeysToBeAdded > threshold) {  
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);  
            if (targetCapacity > MAXIMUM_CAPACITY)  
                targetCapacity = MAXIMUM_CAPACITY;  
            int newCapacity = table.length;  
            while (newCapacity < targetCapacity)  
                newCapacity <<= 1;  
            if (newCapacity > table.length)  
                resize(newCapacity);  
        }  
        // 4. 开始复制(实际上不断调用Put函数插入)  
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())  
            put(e.getKey(), e.getValue());
    }  
    
     /**
       * 函数:remove(Object key)
       * 作用:删除该键值对
       */ 
    
    public V remove(Object key) {  
        Entry<K,V> e = removeEntryForKey(key);  
        return (e == null ? null : e.value);  
    }  
      
    final Entry<K,V> removeEntryForKey(Object key) {  
        if (size == 0) {  
            return null;  
        }  
        // 1. 计算hash值
        int hash = (key == null) ? 0 : hash(key);  
        // 2. 计算存储的数组下标位置
        int i = indexFor(hash, table.length);  
        Entry<K,V> prev = table[i];  
        Entry<K,V> e = prev;  
      
        while (e != null) {  
            Entry<K,V> next = e.next;  
            Object k;  
            if (e.hash == hash &&  
                ((k = e.key) == key || (key != null && key.equals(k)))) {  
                modCount++;  
                size--; 
                // 若删除的是table数组中的元素(即链表的头结点) 
                // 则删除操作 = 将头结点的next引用存入table[i]中  
                if (prev == e) 
                    table[i] = next;
    
                //否则 将以table[i]为头结点的链表中,当前Entry的前1个Entry中的next 设置为 当前Entry的next(即删除当前Entry = 直接跳过当前Entry)
                else  
                    prev.next = next;   
                e.recordRemoval(this);  
                return e;  
            }  
            prev = e;  
            e = next;  
        }  
      
        return e;  
    } 
    
     /**
       * 函数:containsKey(Object key)
       * 作用:判断是否存在该键的键值对;是 则返回true
       * 原理:调用get(),判断是否为Null
       */
       public boolean containsKey(Object key) {  
        return getEntry(key) != null; 
    } 
    
     /**
       * 函数:containsValue(Object value)
       * 作用:判断是否存在该值的键值对;是 则返回true
       */   
    public boolean containsValue(Object value) {  
        // 若value为空,则调用containsNullValue()  
        if (value == null)
            return containsNullValue();  
        
        // 若value不为空,则遍历链表中的每个Entry,通过equals()比较values 判断是否存在
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)  
            for (Entry e = tab[i] ; e != null ; e = e.next)  
                if (value.equals(e.value)) 
                    return true;//返回true  
        return false;  
    }  
      
    // value为空时调用的方法  
    private boolean containsNullValue() {  
        Entry[] tab = table;  
        for (int i = 0; i < tab.length ; i++)  
            for (Entry e = tab[i] ; e != null ; e = e.next)  
                if (e.value == null)
                    return true;  
        return false;  
    } 

    6.0  与jdk1.8中的差别

      数据结构

    示意图

      获取数据

    示意图

       扩容机制

    示意图

      其它对比

    7.0 为什么高并发下会死循环

      多线程下容易出现resize()死循环本质 = 并发 执行 put()操作导致触发 扩容行为,从而导致 环形链表,使得在获取数据遍历链表时形成死循环,即Infinite Loop。

      

    /**
       * 源码分析:resize(2 * table.length)
       * 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
       */ 
       void resize(int newCapacity) {  
        
        // 1. 保存旧数组(old table) 
        Entry[] oldTable = table;  
    
        // 2. 保存旧容量(old capacity ),即数组长度
        int oldCapacity = oldTable.length; 
    
        // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出    
        if (oldCapacity == MAXIMUM_CAPACITY) {  
            threshold = Integer.MAX_VALUE;  
            return;  
        }  
      
        // 4. 根据新容量(2倍容量)新建1个数组,即新table  
        Entry[] newTable = new Entry[newCapacity];  
    
        // 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 
        transfer(newTable); 
    
        // 6. 新数组table引用到HashMap的table属性上
        table = newTable;  
    
        // 7. 重新设置阈值  
        threshold = (int)(newCapacity * loadFactor); 
    } 
    
     /**
       * 分析1.1:transfer(newTable); 
       * 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
       * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入
       */ 
    void transfer(Entry[] newTable) {
          // 1. src引用了旧数组
          Entry[] src = table; 
    
          // 2. 获取新数组的大小 = 获取新容量大小                 
          int newCapacity = newTable.length;
    
          // 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中
          for (int j = 0; j < src.length; j++) { 
              // 3.1 取得旧数组的每个元素  
              Entry<K,V> e = src[j];           
              if (e != null) {
                  // 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
                  src[j] = null; 
    
                  do { 
                      // 3.3 遍历 以该数组元素为首 的链表
                      // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
                      Entry<K,V> next = e.next; 
                     // 3.3 重新计算每个元素的存储位置
                     int i = indexFor(e.hash, newCapacity); 
                     // 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中
                     // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入
                     e.next = newTable[i]; 
                     newTable[i] = e;  
                     // 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
                     e = next;             
                 } while (e != null);
                 // 如此不断循环,直到遍历完数组上的所有数据元素
             }
         }
     }

    注:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况。

    但 JDK 1.8 还是线程不安全,因为 无加同步锁保护

        

  • 相关阅读:
    C语言截取从某位置开始指定长度子字符串方法
    vim:放弃hjkl
    vim资源
    PHP和.NET通用的加密解密函数类,均使用3DES加解密 .
    Java与.NET DES加密解密互转
    案例:使用正则表达式的爬虫
    爬虫的正则表达式re模块
    爬虫中Requests模块
    Oracle系列十一 数据处理
    爬虫urllib2 的异常错误处理URLError和HTTPError
  • 原文地址:https://www.cnblogs.com/helloworldmybokeyuan/p/11713412.html
Copyright © 2020-2023  润新知