说明

本章做一个HashMap的总结、是jdk1.8版本的。

继承图

Java:HashMap内部探究_java

它的继承体系还是比较简单的

抛出问题

  1. 内部数据结构是怎么样的?
  2. 扩容机制是什么时候扩容的、扩容多少?
  3. key或value是否能为空。
  4. 是否线程安全?
HashMap集合说明

创建一个HashMap集合对象

Map<String,Integer> map = new HashMap<>();

属性

//默认初始容量、采用左位移计算、 1 = x; 4 = n; DEFAULT_INITIAL_CAPACITY =  x * 2的n平方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量  --1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的加载因子(负载因子) 、用于扩容使用
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 作为判断、当某个桶数组中的节点元素长度大于8时转红黑树结构
static final int TREEIFY_THRESHOLD = 8;
// 作为判断、当某个节点小于6时,会转换为链表,前提是它当前是红黑树结构
static final int UNTREEIFY_THRESHOLD = 6;
// 当桶数组容量小于该值时,优先进行扩容,而不是树化
static final int MIN_TREEIFY_CAPACITY = 64;
// 存放元素的数组(桶)
transient Node<K,V>[] table;
// 作为迭代使用
transient Set<Map.Entry<K,V>> entrySet;
// 元素数量
transient int size;
// 统计map修改次数
transient int modCount;
// 要调整大小的下一个大小值(容量*负载系数)、当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容
int threshold;
// 加载因子(负载因子)
final float loadFactor;

构造器

/*
    initialCapacity :初始容量
    loadFactor: 负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)  // 小于0就抛出异常
    throw new IllegalArgumentException("Illegal initial capacity: " +
                                       initialCapacity);
  // 如果初始容量大于、最大容量则将最大容量赋值给初始容量
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  // 检查loadFactor是否合法
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
                                       loadFactor);
  this.loadFactor = loadFactor;  //将默认的DEFAULT_LOAD_FACTOR赋值给loadFactor变量
  //对临界值进行初始化,tableSizeFor(initialCapacity)这个方法会返回大于initialCapacity值的,且离其最近的2次幂,例如initialCapacity为29,则返回的值是32**/
  this.threshold = tableSizeFor(initialCapacity);
}
// 自定义初始容量initialCapacity
public HashMap(int initialCapacity) {
  this(initialCapacity, DEFAULT_LOAD_FACTOR); // 调用另一个构造器
}
// 无参的构造
public HashMap() {
  // 它这里做的事情就是将默认的默认负载因子赋值给loadFactor变量
  this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

// 将另一个map集合中的数据放入到当前map集合
public HashMap(Map<? extends K, ? extends V> m) {
  this.loadFactor = DEFAULT_LOAD_FACTOR;
  putMapEntries(m, false);
}

构造要注意的就是这个地方(tableSizeFor(int cap))、返回给定目标容量的二次幂。

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这里有两个知识点一个是有表示无符号右移、也叫逻辑右移 >>>还有一个就是|叫做或运算符、他们都是位运算符

|:如果相对应位都是 0、则结果为 0、否则为 1 如:十进制转二进制计算

13|11

13:1101
11:1011
   1111: 转换后的结果、然后把它转成10进制就是15

对集合CRUD简单说明

public V put(K key, V value) {
            // 主要的添加方法、后面会重点说明一下
    return putVal(hash(key), key, value, false, true);
}

//通过key来移除对应的key的值
public V remove(Object key) {
    Node<K,V> e;
    // removeNode()主要的操作
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value; 返回被删除的key对应的值
}

改和增加是一样的他也是、通过key的hash值来找到对应位置、将其对应值覆盖

//通过key来获取value
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

HashMap的核心内容扩容机制

上面的只是一个简单说明、其实主要的还是他的扩容机制、以及内部实现操作。 我们以添加一个元素来探究

// 添加方法
public V put(K key, V value) {
            // putVal()添加方法
    return putVal(hash(key), key, value, false, true);
}

// 计算key的hash值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//哈希值、 key键、value要放入的值、onlyIfAbsent如果为true,则不更改现有值
//execit如果为false,则表处于创建模式。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
      /* 	tab;数组桶的初始化变量、
  		p:上一个节点值、简单来说就是相同的hash值计算出来的一片区域的节点tab[i = (n - 1) & hash]
  		n:hashMap的长度、
  		i:数组桶表
      */
  Node<K,V>[] tab; Node<K,V> p; int n, i;
  // 这个判断作为第一次添加数据时、做数组桶容量table变量的初始
  if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length; // resize()初始化加扩容方法、最后初始化table的容量为16
  /* 这个判断的作用相当于就是找到key的节点在tab数组桶中的位置、将当前索引的值赋值给p、然后用p来做判断、
  	 是否为null、如果为null则将key|value放入到tab[i]中
  */
  if ((p = tab[i = (n - 1) & hash]) == null)  // 当桶数组中没有这个相同的key是添加
    // 存放数据
    tab[i] = newNode(hash, key, value, null);
  else {
    // e:临时节点存放上一个节点的数据、此值表示如果不为null表示当前链表数据中有一样key存在
    // K:存放上一个节点的key
    Node<K,V> e; K k;
    // 判断是否于上一个key相同
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
      e = p; // 如果不同、将上一个节点p的数据给e
    // 判断上一个节点是否为红黑树节点
    else if (p instanceof TreeNode)
      // 如果是红黑树节点、则在红黑树节点后添加数据、中插入方法中也有判断是否有相同的key存在
      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
      // 遍历所有桶数据中链表的数据、 能到这个else里面就是key的hash值相同、key的内容不同、并且不是红黑树结构的一个判断里面
      for (int binCount = 0; ; ++binCount) {
        // p变量保留的是上一个节点、而p.next找的是下一个节点
        if ((e = p.next) == null) { // 这个判断肯定成立、上一个节点的、下一个节点完全没有、
          // 这个步骤做的是将新节点赋值给上一个节点的下一个节点
          p.next = newNode(hash, key, value, null);
          // 如果链表长度大于等于TREEIFY_THRESHOLD的默认值8的时候
          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            // 进行树化操作、数化的方法中还要满足条件、后面讲解
            treeifyBin(tab, hash);
          break; // 跳出循环
        }
        // 在这个判断就是判断在链表中、下一个节点的key的hash值相同key值相同就break跳出询循环
        // 然后进行下面的if (e != null)其中值替换掉
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
          break;
        p = e;
      }
    }
    /*
    	1、判断上一个节点中的key是否相等的
    	2、判断上一个节点是否为红黑树结构、是则将调用红黑树的插入方法putTreeVal()、
    	这个方法中也判断了是否有重复的key值、将这个重复的key的节点出来赋值给e
    	经过上面二种情况的判断、得出一个结果、如果e != null的情况下、证明有重复的key、要将旧的value
    	值替换当前新添加的值
    */
    if (e != null) { // existing mapping for key
      V oldValue = e.value; // 获取value值
      if (!onlyIfAbsent || oldValue == null)
        e.value = value; // 将新value值重新赋值给存在相同key的节点上的value值
      afterNodeAccess(e);
      return oldValue; // 返回被替换之前的值
    }
  }
  // 修改次数++
  ++modCount;
  // 实际长度++ 如果大于要调整大小的下一个大小值 则需要扩容
  if (++size > threshold)
    resize();  // 扩容
  afterNodeInsertion(evict);
  // 添加成功
  return null;
}

putVal()方法大概做的事情

  • 当table为null时(第一次值的时候)、通过扩容方法resize()来做到table桶数组的初始化

  • 当添加的数据key的hash值相同时?或者是key的hash值相同但是key的值不同时?

    • key的hash值相同时:会找到相同hash值key的这一块区域、会判断他的hash值和key值是否相等、结果为true、将新的value值赋值旧的value、因此来达到key不变、value替换的效果
    • key的hash值相同但是key的值不同时:首先会找到由tab[i = (n - 1) & hash]相同hash值计算出来的一片区域、会判断他的hash值和key值是否相等、结果为false、走else的判断、此时已经拿到了相同hash值计算出来的一片区域、此时下一个节点为null、则将数据存放在下这片区域的下一个节点中、简单来说就是会将key的hash值相同但是key的值不同的数据放在相同hash值创建的这片区域的下一个节点数据中因此达到链表的形式
  • 在做key的hash值相同但是key的值不同时、他会有一个检查链表长度是否大于等于TREEIFY_THRESHOLD(8)属性值的、大于的话就将链表转为红黑树的结构

  • 还一个就是如果hashMap的长度大于threshold(初始值为12)就要进行扩容操作

resize()扩容方法 --HashMap扩容机制

在添加数据的第一次添加数据的时候和每次添加数据的最后都一个判断、一个判断是判断table属性值是否为null还有在每次添加数据的最后都有一个判断集合的元素个数、是否超阈值(这个值就是作为判断是否需要扩容的值)第一次初始扩容将阈值设置为12。

final Node<K,V>[] resize() {
  // 将table桶数组赋值给oldTab、此时oldTab就相当于是一个旧桶数组
  Node<K,V>[] oldTab = table;
  // 获取一下桶数组的长度大小
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  // 获取一下阈值、简单来说就是用来做扩容的判断条件之一
  int oldThr = threshold;
  // 定义新的桶数组大小、和新阈值都为0
  int newCap, newThr = 0;
  // 如果旧桶数组长度大于0的就成立
  if (oldCap > 0) {
    // 判断旧桶长度是否大于等于MAXIMUM_CAPACITY、HashMap的最大容量
    if (oldCap >= MAXIMUM_CAPACITY) {
      //将Integer.MAX_VALUE各大的值赋值给负载系数(阈值) 、并将进行扩容操作
      threshold = Integer.MAX_VALUE;
      // 返回旧桶数组
      return oldTab;
    }
   // 按旧容量和旧阈值的2倍计算新容量和新阈值的大小
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
      newThr = oldThr << 1; // double threshold
  }
  // 如果旧阈值大于0
  else if (oldThr > 0) // initial capacity was placed in threshold
    // 将旧阈值赋值给新桶数组容量
    newCap = oldThr;
  else {               // zero initial threshold signifies using defaults
    // DEFAULT_INITIAL_CAPACITY默认值为16、将其赋值给newCap新桶数组长度大小
    newCap = DEFAULT_INITIAL_CAPACITY;
    // newThr = (容量大小*负载系数)
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  // 如果新阈值等等于0、按阈值计算公式进行计算给出新的阈值
  if (newThr == 0) {
    // 计算新的临界值(阈值) (容量大小*负载系数)  负载系数 =  加载因子 = loadFactor
    float ft = (float)newCap * loadFactor;
    // 判断新容量是否小于最大默认初始容量和、新的临界值是否小于最大默认初始容量
    // 为true将ft新的临界值(阈值)赋值给newThr新阈值
    // 为false将Integer.MAX_VALUE值赋值给newThr新阈值
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
  }
  // 将新的负载系数(阈值)赋值给threshold用于下次扩容
  threshold = newThr;
  //表示忽略该警告
  @SuppressWarnings({"rawtypes","unchecked"})
  // 创建新桶数组、桶的初始化在这里就完成了newCap=16
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  // 将新桶赋值给属性table
  table = newTab;
  // 下面做的判断是旧桶不为null的操作
  if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {
      Node<K,V> e; // 临时存放节点变量
      // 判断桶数组中的元素不等于null
      if ((e = oldTab[j]) != null) {
        oldTab[j] = null; // 设置为null等待垃圾回收
        if (e.next == null) // 判断下一个节点等等于null
          // 把e放在新容量[e.hash & (newCap - 1)求出来的桶数组中]
          newTab[e.hash & (newCap - 1)] = e;
        // 判断e变量是否为红黑树结构
        else if (e instanceof TreeNode)
           // 重新映射时,需要对红黑树进行拆分
          ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
        else { // preserve order
          Node<K,V> loHead = null, loTail = null;
          Node<K,V> hiHead = null, hiTail = null;
          Node<K,V> next;
           // 遍历链表,并将链表节点按原顺序进行分组
          do {
            next = e.next;
            if ((e.hash & oldCap) == 0) {
              if (loTail == null)
                loHead = e;
              else
                loTail.next = e;
              loTail = e;
            }
            else {
              if (hiTail == null)
                hiHead = e;
              else
                hiTail.next = e;
              hiTail = e;
            }
          } while ((e = next) != null);
            // 将分组后的链表映射到新桶中
          if (loTail != null) {
            loTail.next = null;
            newTab[j] = loHead;
          }
          if (hiTail != null) {
            hiTail.next = null;
            newTab[j + oldCap] = hiHead;
          }
        }
      }
    }
  }
 // 返回新桶
  return newTab;
}

resize()扩容做的几个事情

  • 计算出桶数组的新容量(newCap)为多少和新阈值(newThr )为多少
  • 将其计算好的容量和阈值赋值给HashMap类属性、将其做到桶数组的初始化容量大小为:16、扩容形式为 2的n次幂的形式
  • 将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通节点,则节点按原顺序进行分组、细节还请自行探究。

问题解决

  • 内部数据结构是怎么样的?
    • 数组+链表+红黑树
  • 扩容机制是什么时候扩容的、扩容多少?
    • 第一次添加数据时候、初始化容量大小为:16、后续扩容形式为2的n次幂的形式
  • key或value是否能为空。
    • 都可以为null
  • 是否线程安全?
    • 线程不安全

总结

  • HashMap它是Map接口类的实现类之一。

  • 它的键、值可以存放为null的值、但是HashMap最多只允许一条记录的键为null,允许多条记录的值为null

  • 它是一个线程不安全的集合

  • 初始的信息有桶数组为16大小、threshold(阈值 = (容量大小*负载系数) )为12、loadFactor(负载系数)为0.75f、

  • 什么时候扩容?

    • 在第一次添加数据时扩容、进行默认初始桶数组容量大小、和threshold(阈值)大小
    • 在size > threshold(阈值)、长度大于阈值进行扩容
    • 扩容桶长度形式为 2的n次幂的形
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold
        // 具体在 resize()方法中的某一个方法。
    
    
  • HashMap存储结构数组+链表+红黑树

  • 链表数量(元素个数)大于8的情况下将链表转为红黑树结构、大于8说的就是0-7下标这中间有8的元素、所以说这个8就是说链表中元素的个数。

     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
     treeifyBin(tab, hash);
    
    
  • 这里有个***注意点***:

    • treeifyBin(tab, hash);方法中还有一个判断

       //满足这个条件会先进行扩容
       if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
      
      
    • 链表长度大于(默认值8)并且数组长度大于64进行将链表转红黑树、否则、则进行扩容操作

  • 如何计算元素应该放在桶(bucket)数组的那个位置上???

     p = tab[i = (n - 1) & hash]
     p =[i = (桶长度大小-1) 与运算  key的hash值] 
     // 通过(桶长度大小-1)和key的hash值做与运算来得到元素放在桶数组的什么位置上
    
    
  • key的hash值相同、但是key的值不同、他会把这个数据放在由hash值计算出来的这块区域中的下一个节点、简称尾插法

  • 你们可以试试key的hash值相同但value值不同、看第二个值是存放在什么地方、使用idea的debug来看

    Map<String, Object> map1 = new HashMap<>();
    map1.put("通话","2");
    map1.put("重地","4");
    
    

图解

Java:HashMap内部探究_hash_02