Java1.7全网最深入HashMap源码解析

目录
  • 存储结构
  • 属性成员
  • 构造函数:
  • hash方法
  • Map中添加数据
    • put方法
    • 流程图
    • 源码
    • inflateTable方法
    • putForNullKey方法
    • addEntry方法
    • createEntry方法
  • 扩容方法
    • resize方法
    • transfer方法
  • 从HashMap中获取数据
    • get方法
  • 从HashMap中删除数据
    • remove方法
  • 对HashMap的其他操作
  • 1.7和1.8版本区别
    • 数据结构
    • hash值计算方式
    • 扩容机制

存储结构

内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶容量取模运算结果相同的 Entry。

啊啊

transient Entry[] table;  //位桶数组

/**
 * Entry类实现了Map.Entry接口
 * 即 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
**/
static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;  // 键
    V value;  // 值
    Entry<K,V> next; // next指针
    int hash;  //hashCode()方法计算出的hash值

    /**
     * 构造方法,创建一个Entry
     * 参数:哈希值h,键值k,值v、下一个节点n
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }  

    // 返回 与 此项 对应的键
    public final K getKey() {
        return key;
    }  

    // 返回 与 此项 对应的值
    public final V getValue() {
        return value;
    }  

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }  

   /**
     * equals()
     * 作用:判断2个Entry是否相等,必须key和value都相等,才返回true
     */
      public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }  

    /**
     * hashCode()
     */
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }  

    public final String toString() {
        return getKey() + "=" + getValue();
    }  

    /**
     * 当向HashMap中添加元素时,即调用put(k,v)时,
     * 对已经在HashMap中k位置进行v的覆盖时,会调用此方法
     * 此处没做任何处理
     */
    void recordAccess(HashMap<K,V> m) {
    }  

    /**
     * 当从HashMap中删除了一个Entry时,会调用该函数
     * 此处没做任何处理
     */
    void recordRemoval(HashMap<K,V> m) {
    }
}

属性成员

// 1. 容量(capacity): HashMap中数组的长度
// a. 容量范围:必须是2的幂 & <最大容量(2的30次方)
// b. 初始容量 = 哈希表创建时的容量
  // 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

  // 最大容量 =  2的30次方(若传入的容量过大,将被最大值替换)
  static final int MAXIMUM_CAPACITY = 1 << 30;

// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
// a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
// b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
  // 实际加载因子
  final float loadFactor;
  // 默认加载因子 = 0.75
  static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
// a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
// b. 扩容阈值 = 容量 x 加载因子
  int threshold;

    //默认的threshold值
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

// 4. 其他
 // 存储数据的Entry类型 数组,长度 = 2的幂
 // HashMap的实现方式 = 拉链法,Entry数组上的每个元素本质上是一个单向链表
  transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  

//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态
static final Entry<?,?>[] EMPTY_TABLE = {};  

 // HashMap的大小,即 HashMap中存储的键值对的数量
  transient int size;

构造函数:

  • 构造函数仅用于接收初始容量大小(capacity)、负载因子(Load factor),但仍无真正初始化哈希表(存储数组table
  • 此处先给出结论:真正初始化存储数组table是在第1次调用put()添加键值对时

  /**
     * 构造函数1:默认构造函数(无参)
		实际上是调用构造函数3:指定“容量大小”和“加载因子”的构造函数
     */
    public HashMap() {

        // 传入默认的容量(16)和负载因子(0.75)
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 构造函数2:指定“容量大小”的构造函数
     实际上是调用指定“容量大小”和“加载因子”的构造函数
     */
    public HashMap(int initialCapacity) {

        // 传入指定的容量,和默认的负载因子
        this(initialCapacity, DEFAULT_LOAD_FACTOR);

    }

    /**
     * 构造函数3:指定“容量大小”和“加载因子”的构造函数
     * 加载因子 & 容量都由自己指定
     */
    public HashMap(int initialCapacity, float loadFactor) {

        // HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的 > 最大容量
        //如果大于最大容量,还是赋值为1 << 30
        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);
    }
}

hash方法

hash(Object k):计算key的hash值

该函数在JDK 1.7 和 1.8 中的实现不同,但原理(扰动函数)一样使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)

  • JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
  • JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
   /**
     * 确定位桶数组下标主要分为2步:计算hash值、根据hash值再计算得出最后数组位置
     */
        // a. 根据键值key计算hash值 ->> 分析1
        int hash = hash(key);
        // b. 根据hash值 最终获得 key对应存放的数组Table中位置 ->> 分析2
        int i = indexFor(hash, table.length);

     // 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的位置(即数组下标、索引)
}

Map中添加数据

put方法

put(int hash, K key, V value, int bucketIndex):向HashMap添加数据(成对存放 key - value)

流程图

源码

 /**
   * 函数使用原型
   */
   map.put("name", "huangkaiyu");
   map.put("age", 21);

    public V put(K key, V value)
		// 1.如果哈希表未初始化(即 table为空)
        if (table == EMPTY_TABLE) {
        // 则使用构造函数传入的阈值(即初始容量) 初始化数组table
        inflateTable(threshold);
    }  

        // 2. 判断key是否为空值null
		// 若key == null,则将该键值对放在table [0](本质:key = Null时,hash值 = 0,故存放到table[0]中)
        if (key == null)
            return putForNullKey(value);

		 //若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)

		//计算hash值
        int hash = hash(key);
       //传入hash值和table长度算出index
        int i = indexFor(hash, table.length);

        // 3. 遍历table[indexFor]对应的链表判断该key对应的值是否已存在
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
    	//若该key已存在(即 key-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
            }
        }
		//改动计数器+1
        modCount++;

		// 若该key不存在,则将“key-value”添加到table中
        addEntry(hash, key, value, i);
        return null;
    }

inflateTable方法

inflateTable(int toSize):初始化数组(table)、扩容阈值(threshold

注意:

真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()

   /**
     * put中调用
     */
      if (table == EMPTY_TABLE) {
         //此处传入的是构造函数时设置的阈值(即初始容量),不是真正的扩容阈值
        inflateTable(threshold);
    }  

     private void inflateTable(int toSize) {  

    // 1. 将传入的容量大小转化为:>传入容量大小的最小的2的次幂(传入18转化得32)
    int capacity = roundUpToPowerOf2(toSize);   

    // 2. 重新计算阈值 threshold = 容量 * 加载因子(之前存的是容量)
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  

    // 3.传入容量初始化位桶数组table(作为数组长度)
    table = new Entry[capacity];  

    initHashSeedAsNeeded(capacity);
}  

    /**
     * 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;

putForNullKey方法

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

   /**
     *  put()方法调用时 传入的key为空
     */
      if (key == null)
           return putForNullKey(value);

   	 /**
     * 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
     	有就替换并返回旧值,没有就调用addEntry()将(null,value)添加到链表中
     */
      private V putForNullKey(V value) {  

        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
          //如果链表结点key为空
          if (e.key == null) {
            //保存旧值
            V oldValue = e.value;
            //链表赋新值
            e.value = value;
            e.recordAccess(this);
             //返回旧值
            return oldValue;
        }
    }
    //改动次数+1
    modCount++;  

    // 若没有key==null的键,那么调用addEntry()将其加入链表
    addEntry(0, null, value, 0); 

    // a. addEntry()的第1个参数hash值传入0(当key = null时,也有hash值 = 0,所以HashMap的key 可为null)

    // c. 对比HashTable,由于HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
    // d. 此处只需知道是将 key-value 添加到HashMap中即可,关于addEntry()的源码分析将等到下面再详细说明,
    return null;  

}

addEntry方法

addEntry(int hash, K key, V value, int bucketIndex):添加键值对(Entry )到 HashMap中

      /**
        * put中key不存在调用,将Entry对象存入链表
        */

      void addEntry(int hash, K key, V value, int bucketIndex) {  

          // 1. 插入前先判断是否需要扩容

          // 如果元素个数>=扩容阈值 并且 对应数组下标不为空
          if ((size >= threshold) && (null != table[bucketIndex])) { 

            //扩容2倍
            resize(2 * table.length);
             // 重新计算Key对应的hash值
            hash = (null != key) ? hash(key) : 0;
          	 // 重新计算该Key对应的hash值的存储数组下标位置
            bucketIndex = indexFor(hash, table.length);
    }  

    //如果不需要扩容,则创建1个新的Entry并放入到数组中
    createEntry(hash, key, value, bucketIndex);
}

createEntry方法

createEntry(int hash, K key, V value, int bucketIndex)

 /**
   * 分析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++;
}

扩容方法

resize方法

resize(int newCapacity):扩容为原来两倍

在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况

设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1

此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 = 线程不安全

 /**
   * resize(2 * table.length)
   * 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
   */
   void resize(int newCapacity) {  

    // 1. 保存旧数组(old table)
    Entry[] oldTable = table;  

    // 2. 保存旧容量(数组长度)
    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中,从而完成扩容
    transfer(newTable); 

    // 6. 新数组table引用到HashMap的table属性上
    table = newTable;  

    // 7. 重新设置阈值
    threshold = (int)(newCapacity * loadFactor);
}

transfer方法

transfer(Entry[] newTable):

/**
   * transfer(newTable);
   * 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容
   * 过程:按旧链表的正序遍历链表采用头插法插入新链表
   */
void transfer(Entry[] newTable) {
      // 1. src指向原table
      Entry[] src = table; 

      // 2. 获取新数组的大小
      int newCapacity = newTable.length;

      // 3. 通过遍历旧table,将键值对转移到新table上
      for (int j = 0; j < src.length; j++) { 

      	  // 创建辅助entry指向旧数组中的元素
          Entry<K,V> e = src[j];

          if (e != null) {
              // 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象)
              src[j] = null;
			  //开始遍历
              do { 

				//创建辅助指针next(因是单链表,故要保存下1个结点,否则转移后链表会断开	)
                 Entry<K,V> next = e.next;
                 // 重新计算每个元素的存储位置
                 int i = indexFor(e.hash, newCapacity); 

                  //头插法插入
                 e.next = newTable[i];
                 newTable[i] = e;

                 // e跳到下一个entry
                 e = next;
             } while (e != null);
             // 循环直到遍历完数组上的所有数据元素
         }
     }
 }

从HashMap中获取数据

get方法

public V get(Object key):根据键key,向HashMap获取对应的值

   public V get(Object key) {  

    // 1. 当key == null时,则到table[0]为头结点的链表去检索
    if (key == null)
        return getForNullKey();

    // 2. 当key ≠ null时,去获得对应值
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}  

 /**
   * getForNullKey()
   * 作用:当key == null时,在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;
}  

 /**
   * getEntry(key)
   * 作用:当key ≠ null时,去获得对应值
   */
final Entry<K,V> getEntry(Object key) {  

    //如果元素个数为空返回null
    if (size == 0) {
        return null;
    }  

    // 1. 计算key对应的hash值
    int hash = (key == null) ? 0 : hash(key);  

    // 2. 根据hash值计算出对应的数组下标
    // 3. 遍历对应index的数组元素为头结点的链表,检索键值对
    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;
}

从HashMap中删除数据

remove方法

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);  

    //prev记录要删除entry的前一个entry
    Entry<K,V> prev = table[i];  

    //e记录要删除的entry
    Entry<K,V> e = prev;  

    while (e != null) {
        //辅助指针,指向下一个entry
        Entry<K,V> next = e.next;
        Object k;
        //如果key相等
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++; //改动次数+1

            size--;   //元素个数-1

            // 若删除的是链表的头结点
            if (prev == e)
               // 则将头结点的next引用存入table[i]中
                table[i] = next;

            //否则 将当前结点的前1个结点中的next指向当前结点的下一个结点(直接越过当前Entry)
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }

        //prev指向当前结点
        prev = e;
        //e指向下一个结点
        e = next;
    }
    //遍历结束e为null,表示没找到返回null
    return e;
}

对HashMap的其他操作

HashMap除了核心的put()get()函数,还有以下主要使用的函数方法

void clear(); 清除哈希表中的所有键值对
int size(); 返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); 判断HashMap是否为空;size == 0时 表示为 空
void putAll(Map<? extends K, ? extends V> m); 将指定Map中的键值对 复制到 此Map中
boolean containsKey(Object key); 判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); 判断是否存在该值的键值对;是 则返回true

源码

  /**
   * 函数: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() {
    //改动次数+1
    modCount++;
    //全部元素设空
    Arrays.fill(table, null);
    //元素个数清0
    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());
}  

 /**
   * 函数: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;
}  

// 判断是否有空值
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;
}

1.7和1.8版本区别

JDK 1.8 的优化目的主要是:减少 Hash冲突 & 提高哈希表的存、取效率

数据结构

版本 存储结构 数组&链表结点实现类 红黑树的实现类 初始化方式
JDK1.7 数组+链表 Entry类 无红黑树 单独函数:inflateTable()
JDK1.8 数组+链表+红黑树 Node类 TreeNode类 直接集成在扩容函数:resize()中

hash值计算方式

版本 hash值计算方式
JDK1.7 1.hashcode计算
JDK1.8 按照扩容后的规律计算(扩容后的位置=原位置 or 原位置 +旧容量)

扩容机制

版本 重hash计算位置
JDK1.7 1.Object.hashCode计算
2. 9次扰动处理 =4次位运算+5次异或运算
JDK1.8 1.Object.hashCode计算
2. 2次扰动处理 =1次位运算+1次异或运算

到此这篇关于Java1.7全网最深入HashMap源码解析的文章就介绍到这了,更多相关Java HashMap 源码解析内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JAVA核心知识之ConcurrentHashMap源码分析

    1 前言 ConcurrentHashMap是基于Hash表的Map接口实现,键与值均不允许为NULL,他是一个线程安全的Map.同时他也是一个无序的Map,不同时间进行遍历可能会得到不同的顺序.在JDK1.8之前,ConcurrentHashMap使用分段锁以在保证线程安全的同时获得更大的效率.JDK1.8开始舍弃了分段锁,使用自旋+CAS+sync关键字来实现同步.本文所述便是基于JDK1.8. ConcurrentHashMap与HashMap有共同之处,一些HashMap的基本概念与实现

  • 浅谈Java源码ConcurrentHashMap

    一.记录形式 打算直接把过程写在源码中,会按序进行注释,查阅的时候可以按序号只看注释部分 二.ConcurrentHashMap 直接模拟该类的使用过程,从而一步步看其怎么运作的吧,当然最好还是带着问题一遍思考一遍总结会比较好,我阅读源码的时候带着以下几个问题 并发体现在哪里?怎么保证线程安全的 怎么扩容的?扩容是怎么保证线程安全的? 怎么put的?put是怎么保证线程安全的? 用了哪些锁?这些锁的作用是什么? 需要留意哪些关键点? 我们最简单地使用方法是怎么样的? new一个Concurren

  • Java源码解析之LinkedHashMap

    一.成员变量 先来看看存储元素的结构吧: static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } 这个Entry在HashMap中被引用过,主要是为了能让LinkedHashMap也支持树化.

  • java基础类型源码解析之多角度讲HashMap

    前言 终于来到比较复杂的HashMap,由于内部的变量,内部类,方法都比较多,没法像ArrayList那样直接平铺开来说,因此准备从几个具体的角度来切入. 桶结构 HashMap的每个存储位置,又叫做一个桶,当一个Key&Value进入map的时候,依据它的hash值分配一个桶来存储. 看一下桶的定义:table就是所谓的桶结构,说白了就是一个节点数组. transient Node<K,V>[] table; transient int size; 节点 HashMap是一个map结

  • Java源码解析之ConcurrentHashMap

    早期 ConcurrentHashMap,其实现是基于: 分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放. HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的

  • Java源码解析之HashMap的put、resize方法详解

    一.HashMap 简介 HashMap 底层采用哈希表结构 数组加链表加红黑树实现,允许储存null键和null值 数组优点:通过数组下标可以快速实现对数组元素的访问,效率高 链表优点:插入或删除数据不需要移动元素,只需要修改节点引用效率高 二.源码分析 2.1 继承和实现 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

  • java集合类HashMap源码解析

    Map集合 Map集合存储的是键值对 Map集合的实现类: HashTable.LinkedHashMap.HashMap.TreeMap HashMap 基础了解: 1.键不可以重复,值可以重复: 2.底层使用哈希表实现: 3.线程不安全: 4.允许key为null,但只允许有一条记录为null,value也可以为null,允许多条记录为null: 源码分析 (一)以JDK1.7为例 1.存储结构 数据结构:数组+链表 首先hashmap内部有一个Entry类型的数组table: 通过Entr

  • Java详解HashMap实现原理和源码分析

    目录 学习要点: 1.什么是HashMap? 2.HashMap的特性 3.HashMap的数据结构 4.HashMap初始化操作 4.1.成员变量 4.2. 构造方法 5.Jdk8中HashMap的算法 5.1.HashMap中散列算法 5.2.什么是HashMap中哈希冲突? 6.Jdk8中HashMap的put操作 7.HashMap的扩容机制 7.1.什么时候需要扩容? 7.2.什么是HashMap的扩容? 7.3.resize的源码实现 8.Jdk8中HashMap的remove操作

  • Java1.7全网最深入HashMap源码解析

    目录 存储结构 属性成员 构造函数: hash方法 Map中添加数据 put方法 流程图 源码 inflateTable方法 putForNullKey方法 addEntry方法 createEntry方法 扩容方法 resize方法 transfer方法 从HashMap中获取数据 get方法 从HashMap中删除数据 remove方法 对HashMap的其他操作 1.7和1.8版本区别 数据结构 hash值计算方式 扩容机制 存储结构 内部包含了一个 Entry 类型的数组 table.E

  • 深入理解Java之HashMap源码剖析

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保证该顺序恒久不变. 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap. Map map = Coll

  • Java集合系列之HashMap源码分析

    前面我们已经分析了ArrayList和LinkedList这两个集合,我们知道ArrayList是基于数组实现的,LinkedList是基于链表实现的.它们各自有自己的优劣势,例如ArrayList在定位查找元素时会优于LinkedList,而LinkedList在添加删除元素时会优于ArrayList.而本篇介绍的HashMap综合了二者的优势,它的底层是基于哈希表实现的,如果不考虑哈希冲突的话,HashMap在增删改查操作上的时间复杂度都能够达到惊人的O(1).我们先看看它所基于的哈希表的结

  • HashMap源码中的位运算符&详解

    引言 最近在读HashMap源码的时候,发现在很多运算符替代常规运算符的现象.比如说用hash & (table.length-1) 来替代取模运算hash&(table.length):用if((e.hash & oldCap) == 0)判断扩容后元素的位置等等. 1.取模运算符%底层原理 ​总所周知,位运算&直接对二进制进行运算:而对于取模运算符%:a % b 相当于 a - a / b * b,底层实际上是除法器,究其根源也是由底层的减法和加法共同完成.所以其运行效

  • Java HashMap源码及并发环境常见问题解决

    HashMap源码简单分析: 1 一切需要从HashMap属性字段说起: /** The default initial capacity - MUST be a power of two. 初始容量 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by eit

  • Java HashMap源码深入分析讲解

    1.HashMap是数组+链表(红黑树)的数据结构. 数组用来存放HashMap的Key,链表.红黑树用来存放HashMap的value. 2.HashMap大小的确定: 1) HashMap的初始大小是16,在下面的源码分析中会看到. 2)如果创建时给定大小,HashMap会通过计算得到1.2.4.8.16.32.64....这样的二进制位作为HashMap数组的大小. //如何做到的呢?通过右移和或运算,最终n = xxx11111.n+1 = xx100000,2的n次方,即为数组大小 s

  • 源码解析JDK 1.8 中的 Map.merge()

    Map 中ConcurrentHashMap是线程安全的,但不是所有操作都是,例如get()之后再put()就不是了,这时使用merge()确保没有更新会丢失. 因为Map.merge()意味着我们可以原子地执行插入或更新操作,它是线程安全的. 一.源码解析 default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) { Objects.requireNonNu

  • 详解Redis 缓存删除机制(源码解析)

    删除的范围 过期的 key 在内存满了的情况下,如果继续执行 set 等命令,且所有 key 都没有过期,那么会按照缓存淘汰策略选中的 key 过期删除 redis 中设置了过期时间的 key 会单独存储一份 typedef struct redisDb { dict *dict; // 所有的键值对 dict *expires; //设置了过期时间的键值对 // ... } redisDb; 设置有效期 Redis 中有 4 个命令可以给 key 设置过期时间,分别是 expire pexpi

随机推荐