概览

  • HashMap类使用散列表实现Map接口并扩展AbstractMap。HashMap采用哈希算法实现, 底层采用了哈希表存储数据,用 Node数组 实现
  • HashMap 根据键的 hashCode 值计算出hash值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。
  • HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。
  • HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。
  • 如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap

image.png

关键设计

概念

  • size : HashMap中存放KV的数量(为链表和树中的KV的总和)
  • capacity :指HashMap中桶node的数量。默认值为16。一般第一次扩容时会扩容到64,之后好像是2倍。总之,容量都是2的幂。capacity 始终大于size
  • loadFactor :装载因子,计算HashMap的实时装载因子的方法为:size/capacity
  • threshold :hashmap所能容纳的最大数据量的Node个数,当HashMap的size大于threshold时会执行resize操作。 初始化时threshold=capacity*loadFactor

属性

  1. transient Node<K,V>[] table;
  2. transient Set<Map.Entry<K,V>> entrySet;
  3. transient int modCount;
  4. transient int size; // HashMap中存放KV的数量(为链表和树中的KV的总和)
  5. final float loadFactor; // 装载因子(如果值太高, 虽说会提高空间利用率, 但是加大查找的开销)
  6. int threshold; //下次扩容阀值(容量*加载因子) threshold=capacity*loadFactor
  7. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量大小 16
  8. static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量2^30
  9. static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认填充比
  10. static final int TREEIFY_THRESHOLD = 8; // 由链表转换成树的阈值
  11. static final int UNTREEIFY_THRESHOLD = 6; // 由树转换成链表的阈值
  12. // 当桶中的bin被树化时最小的hash表容量。
  13. // 可树化的最小数组容量
  14. //(如果表中的节点太多,则重新调整表大小)这个值至少是TREEIFY_THRESHOLD的4倍
  15. static final int MIN_TREEIFY_CAPACITY = 64;

存储单元
node是hashmap的一个内部类,用来储存数据和保持链表结构的。它的本质就是一个映射(键值对)。

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2. final int hash;
  3. final K key;
  4. V value;
  5. Node<K,V> next;
  6. // 重写hashCode
  7. public final int hashCode() {
  8. return Objects.hashCode(key) ^ Objects.hashCode(value);
  9. }
  10. // 重写 equals
  11. public final boolean equals(Object o) {
  12. if (o == this)
  13. return true;
  14. if (o instanceof Map.Entry) {
  15. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  16. if (Objects.equals(key, e.getKey()) &&
  17. Objects.equals(value, e.getValue()))
  18. return true;
  19. }
  20. return false;
  21. }
  22. }

设置容量大小
threshold = 返回 不小于给定目标容量cap的最小二次幂

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

构造器

  1. public HashMap() // 默认填充比为0.75
  2. // threshold=(最小2次幂>=initialCapacity),默认填充比为0.75
  3. // =HashMap(initialCapacity, DEFAULT_LOAD_FACTOR)
  4. public HashMap(int initialCapacity)
  5. public HashMap(int initialCapacity, float loadFactor) // 指定容量,指定填充比
  6. {
  7. if (initialCapacity < 0) //如果初始容量小于0,抛出异常
  8. throw new IllegalArgumentException("Illegal initial capacity: " +
  9. initialCapacity);
  10. if (initialCapacity > MAXIMUM_CAPACITY)
  11. initialCapacity = MAXIMUM_CAPACITY;
  12. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  13. throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
  14. this.loadFactor = loadFactor;
  15. this.threshold = tableSizeFor(initialCapacity);
  16. }
  17. public HashMap(Map<? extends K, ? extends V> m)

hash

取key的hashCode值、高位运算、取模运算

  1. static final int hash(Object key) {
  2. int h;
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }

jdk1.8确定map坐标的方式是tab[(n-1)&hash] n代表map的length,由于绝大多数情况下 map的length的值小于2^16 (25536),所以大部分情况下是hash的低16位与map的length进行与运算(&)

  • 当length=8时 下标运算结果取决于哈希值的低三位
  • 当length=16时 下标运算结果取决于哈希值的低四位
  • 当length=32时 下标运算结果取决于哈希值的低五位
  • 当length=2的N次方, 下标运算结果取决于哈希值的低N位。

    resize扩容

  1. 原容量大于0,需要扩容,阈值扩容至原来的两倍,但最大值为Integer.MAX_VALUE
  2. 原数组=null,原阈值>0,初始容量设置为原阈值,新阈值设置为新容量0.75,即原阈值0.75(通常是指定容量的构造器 初始化扩容)
  3. 原数组=null,原阈值=0,采用默认值,容量=16,阈值=16*0.75(无参构造器初始化扩容)
  4. 保证新阈值为新容量*0.75
  5. 若原数组中存在值,需拷贝到新数组,遍历数组,数组位置上不为空,需转移,且把原引用置为null

    1. 原数组位置只有一个节点,根据hash计算下标存放到新数组
    2. 不止一个节点,且当前节点为树节点,按红黑树管理存入新数组
    3. 链表,按照新数组的下标计算方式存入新数组
      1. final Node<K,V>[] resize() {
      2. Node<K,V>[] oldTab = table;//oldTab变量指向原来的数组
      3. int oldCap = (oldTab == null) ? 0 : oldTab.length;
      4. int oldThr = threshold; //oldThr变量保存原来数组的临界值
      5. int newCap, newThr = 0;
      6. if (oldCap > 0) {//说明将要进行扩容操作
      7. if (oldCap >= MAXIMUM_CAPACITY) {
      8. //由于最大容量不能超过MAXMUM_CAPACITY, 当原数组容量达到该值后不能再扩容
      9. threshold = Integer.MAX_VALUE;
      10. return oldTab;
      11. }
      12. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
      13. oldCap >= DEFAULT_INITIAL_CAPACITY)
      14. newThr = oldThr << 1; // double threshold 进行两倍扩容
      15. }
      16. else if (oldThr > 0) // initial capacity was placed in threshold
      17. // oldCap=0, 说明原来的table数组为null
      18. // 初始容量设置为阈值
      19. newCap = oldThr;
      20. else { // zero initial threshold signifies using defaults
      21. //oldCap=0, oldThr=0,所以一切参数采用默认值
      22. newCap = DEFAULT_INITIAL_CAPACITY;
      23. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
      24. }
      25. if (newThr == 0) {
      26. float ft = (float)newCap * loadFactor;//新容器的临界值
      27. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
      28. (int)ft : Integer.MAX_VALUE);
      29. }
      30. threshold = newThr;
      31. @SuppressWarnings({"rawtypes","unchecked"})
      32. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建新容量的数组
      33. table = newTab;
      34. if (oldTab != null) {//如果原数组中存在值, 需要将原来数组中的值保存到新数组中
      35. for (int j = 0; j < oldCap; ++j) {//遍历原来的数组
      36. Node<K,V> e;
      37. if ((e = oldTab[j]) != null) {//如果原数组位置中的值!=null, 则需进行转移
      38. oldTab[j] = null;//置为null, 方便进行GC
      39. if (e.next == null)
      40. //原数组中保存的hash值是没有冲突的, 也就是Node类型变量
      41. newTab[e.hash & (newCap - 1)] = e;
      42. else if (e instanceof TreeNode)
      43. // 原数组中保存的hash值存在冲突, 是红黑树 TreeNode 类型变量,
      44. // 采用红黑树管理冲突的键值对
      45. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
      46. else { // preserve order
      47. // 原数组中保存的hash值存在冲突,
      48. //但是并没有用红黑树对冲突的Hash值进行管理, 而是用Node链表进行管理
      49. Node<K,V> loHead = null, loTail = null;
      50. Node<K,V> hiHead = null, hiTail = null;
      51. Node<K,V> next;
      52. do {
      53. next = e.next;
      54. //因为需要根据冲突链表中的hash值存放到新数组中,而新数组的长度是原数组长度的2倍,
      55. //newTable.length-1 比 oldTable.length-1 多oldCap, 因此 hash&(newTable.length-1)
      56. //等价于 hash&(oldTable.length-1) + (hash&oldCap ==0 ? 0 : oldCap)
      57. if ((e.hash & oldCap) == 0) {
      58. if (loTail == null)
      59. loHead = e;
      60. else
      61. loTail.next = e;
      62. loTail = e;
      63. }
      64. else {
      65. if (hiTail == null)
      66. hiHead = e;
      67. else
      68. hiTail.next = e;
      69. hiTail = e;
      70. }
      71. } while ((e = next) != null);
      72. //将链表复制到新数组中
      73. if (loTail != null) {
      74. loTail.next = null;
      75. newTab[j] = loHead;
      76. }
      77. if (hiTail != null) {
      78. hiTail.next = null;
      79. newTab[j + oldCap] = hiHead;
      80. }
      81. }
      82. }
      83. }
      84. }
      85. return newTab;
      86. }

      put

      1. public V put(K key, V value) {
      2. return putVal(hash(key), key, value, false, true);
      3. }
      putVal
  6. 判断table是否为空,若空为其初始化扩容

  7. 根据 hash值 计算获得table下标 i= (table.length - 1) & hash 等价于对length取模,也就是hash %length(位运算只能用于除数是2的n次方的数的求余)
  8. 若table [i] == null ,填入添加的映射, size++,若size > threshold,扩容,return null;
  9. 若table [i] != null,判断当前位置的key是不是与将要存入的key相同,判断当前节点是否为树节点

    1. 判断当前位置,链头 的key 是否与将要存入的key相同,相同覆盖value,return原value
    2. key不同,判断p节点是否为树节点;若是,加入红黑树
    3. key不同,不是树节点,则遍历链表,从头开始

      1. 若找到,代表是修改,覆盖原value,return原value
      2. 在链表中未找到与将要存入的key相同的key,代表是新增,若添加节点后,节点数>=8,将链表转化为红黑树,return原value

        1. @param hash keyhash
        2. @param onlyIfAbsent if true, don't change existing value
        3. @param evict if false, the table is in creation mode. 表处于创建模式
        4. @return previous value, or null if none
        5. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        6. // tab=初始map数组 ; n=初始map数组得长度
        7. // p=hash计算后map数组 ; i=hash计算后map数组下标位置
        8. Node<K,V>[] tab; Node<K,V> p; int n, i;
        9. if ((tab = table) == null || (n = tab.length) == 0)
        10. // 当数组table为null时, 调用resize生成数组table, 并令tab指向数组table
        11. n = (tab = resize()).length;
        12. // 计算下标 将数据 赋值给 p (hash计算后map数组)
        13. // 当前下标无数据 如果新存放的hash值没有冲突
        14. if ((p = tab[i = (n - 1) & hash]) == null)
        15. // 则只需要生成新的Node节点并存放到table数组中即可
        16. tab[i] = newNode(hash, key, value, null);
        17. else {
        18. // 否则就是产生了hash冲突
        19. Node<K,V> e; K k;
        20. if (p.hash == hash &&
        21. ((k = p.key) == key || (key != null && key.equals(k))))
        22. // 如果hash值相等且key值相等, 则令e指向冲突的头节点
        23. e = p;
        24. else if (p instanceof TreeNode)
        25. // 如果头节点的key值与新插入的key值不等,
        26. // 并且头结点是TreeNode类型,说明该hash值冲突是采用红黑树进行处理.
        27. // 向红黑树中插入新的Node节点
        28. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        29. else {
        30. // 否则就是采用链表处理hash值冲突
        31. // 遍历冲突链表, binCount记录hash值冲突链表中节点个数
        32. for (int binCount = 0; ; ++binCount) {
        33. if ((e = p.next) == null) {
        34. // 当遍历到冲突链表的尾部时
        35. // 生成新节点添加到链表末尾
        36. p.next = newNode(hash, key, value, null);
        37. // 如果binCount即冲突节点的个数>= (TREEIFY_THRESHOLD(=8) - 1),
        38. //便将冲突链表改为红黑树结构,否则不需要改为红黑树结构
        39. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
        40. treeifyBin(tab, hash);
        41. break;
        42. }
        43. // 如果在冲突链表中找到相同key值的节点, 则直接用新value覆盖原来value值
        44. if (e.hash == hash &&
        45. ((k = e.key) == key || (key != null && key.equals(k))))
        46. break;
        47. p = e;
        48. }
        49. }
        50. // 说明原来已经存在相同key的键值对
        51. if (e != null) { // existing mapping for key
        52. V oldValue = e.value;
        53. if (!onlyIfAbsent || oldValue == null)
        54. //onlyIfAbsent为true表示仅当<key,value>不存在时进行插入, 为false表示强制覆盖
        55. e.value = value;
        56. afterNodeAccess(e);
        57. return oldValue;
        58. }
        59. }
        60. ++modCount;// 修改次数自增
        61. if (++size > threshold)
        62. // 当键值对数量size达到临界值threhold后, 需要进行扩容操作
        63. resize();
        64. afterNodeInsertion(evict);
        65. return null;
        66. }

        putTreeVal

        1. final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
        2. Class<?> kc = null;
        3. boolean searched = false;
        4. TreeNode<K,V> root = (parent != null) ? root() : this;
        5. for (TreeNode<K,V> p = root;;) {
        6. int dir, ph; K pk;
        7. if ((ph = p.hash) > h)
        8. dir = -1;
        9. else if (ph < h)
        10. dir = 1;
        11. else if ((pk = p.key) == k || (k != null && k.equals(pk)))
        12. return p;
        13. else if ((kc == null &&
        14. (kc = comparableClassFor(k)) == null) ||
        15. (dir = compareComparables(kc, k, pk)) == 0) {
        16. if (!searched) {
        17. TreeNode<K,V> q, ch;
        18. searched = true;
        19. if (((ch = p.left) != null &&
        20. (q = ch.find(h, k, kc)) != null) ||
        21. ((ch = p.right) != null &&
        22. (q = ch.find(h, k, kc)) != null))
        23. return q;
        24. }
        25. dir = tieBreakOrder(k, pk);
        26. }
        27. TreeNode<K,V> xp = p;
        28. if ((p = (dir <= 0) ? p.left : p.right) == null) {
        29. Node<K,V> xpn = xp.next;
        30. TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
        31. if (dir <= 0)
        32. xp.left = x;
        33. else
        34. xp.right = x;
        35. xp.next = x;
        36. x.parent = x.prev = xp;
        37. if (xpn != null)
        38. ((TreeNode<K,V>)xpn).prev = x;
        39. moveRootToFront(tab, balanceInsertion(root, x));
        40. return null;
        41. }
        42. }
        43. }

treeifyBin

  1. //该方法的主要作用是将冲突链表改为红黑树
  2. final void treeifyBin(Node<K,V>[] tab, int hash) {
  3. int n, index; Node<K,V> e;
  4. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
  5. //当数组的长度< MIN_TREEIFY_CAPACITY(64) 时,只是单纯将数组扩容, 而没有直接将链表改为红黑树
  6. //因为hash数组长度还太小时导致多冲突的主要原因, 增大hash数组长度可以改善冲突情况
  7. resize();
  8. else if ((e = tab[index = (n - 1) & hash]) != null) {
  9. TreeNode<K,V> hd = null, tl = null;
  10. do {
  11. TreeNode<K,V> p = replacementTreeNode(e, null);
  12. if (tl == null)
  13. hd = p;
  14. else {
  15. p.prev = tl;
  16. tl.next = p;
  17. }
  18. tl = p;
  19. } while ((e = e.next) != null);
  20. if ((tab[index] = hd) != null)
  21. hd.treeify(tab);
  22. }
  23. }

get

思路如下:

  1. bucket里的第一个节点,直接命中;
  2. 如果有冲突,则通过key.equals(k)去查找对应的entry
    1. 若为树,则在树中通过key.equals(k)查找,O(logn);
    2. 若为链表,则在链表中遍历通过key.equals(k)查找,O(n)。
  1. public V get(Object key) {
  2. Node<K, V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }
  5. final Node<K, V> getNode(int hash, Object key) {
  6. Node<K, V>[] tab;
  7. Node<K, V> first, e;
  8. int n;
  9. K k;
  10. // first指向hash值对应数组位置中的Node节点
  11. if ((tab = table) != null && (n = tab.length) > 0
  12. && (first = tab[(n - 1) & hash]) != null) {
  13. // 如果first节点对应的hash和key的hash相等
  14. //(在数组相同位置,只是说明 hash&(n-1) 操作结果相等, 说明hash值的部分低位相等,并不代表整个hash值相等),
  15. // 并且first对应的key也相等的话, first节点就是要查找的
  16. if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
  17. return first;
  18. if ((e = first.next) != null) { // 说明存在hash冲突
  19. if (first instanceof TreeNode) // 说明由红黑树对hash值冲突进行管理
  20. return ((TreeNode<K, V>) first).getTreeNode(hash, key); // 查找红黑树
  21. do { // 说明hash值冲突是由链表进行管理
  22. if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
  23. return e;
  24. } while ((e = e.next) != null); // 对链表进行遍历
  25. }
  26. }
  27. return null;
  28. }