存储结构

数组+链表+红黑树,1.8版本当链表节点较少时仍然是以链表存在,当链表节点较多时(大于8)会转为红黑树

几个属性

  1. // 默认容量16
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  3. // 最大容量
  4. static final int MAXIMUM_CAPACITY = 1 << 30;
  5. // 默认负载因子0.75
  6. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  7. // 链表节点转换红黑树节点的阈值, 9个节点转
  8. static final int TREEIFY_THRESHOLD = 8;
  9. // 红黑树节点转换链表节点的阈值, 6个节点转
  10. static final int UNTREEIFY_THRESHOLD = 6;
  11. // 转红黑树时, table的最小长度
  12. static final int MIN_TREEIFY_CAPACITY = 64;
  13. // 链表节点, 继承自Entry
  14. static class Node<K,V> implements Map.Entry<K,V> {
  15. final int hash;
  16. final K key;
  17. V value;
  18. Node<K,V> next;
  19. // ... ...
  20. }
  21. // 红黑树节点
  22. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  23. TreeNode<K,V> parent; // red-black tree links
  24. TreeNode<K,V> left;
  25. TreeNode<K,V> right;
  26. TreeNode<K,V> prev; // needed to unlink next upon deletion
  27. boolean red;
  28. // ...
  29. }

初始化

无参方法
使用默认负载因子 0.75

  1. public HashMap() {
  2. this.loadFactor = DEFAULT_LOAD_FACTOR;
  3. }

有参方法
可以填默认初始化大小,以及负载因子

  1. public HashMap(int initialCapacity) {
  2. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  3. }
  4. public HashMap(int initialCapacity, float loadFactor) {
  5. if (initialCapacity < 0)
  6. throw new IllegalArgumentException("Illegal initial capacity: " +
  7. initialCapacity);
  8. if (initialCapacity > MAXIMUM_CAPACITY)
  9. initialCapacity = MAXIMUM_CAPACITY;
  10. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  11. throw new IllegalArgumentException("Illegal load factor: " +
  12. loadFactor);
  13. this.loadFactor = loadFactor;
  14. this.threshold = tableSizeFor(initialCapacity);
  15. }
  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. }

这个算法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数),该算法让最高位的1后面的位全变为1,详情可参考 HashMap中tableSizeFor

put(K key, V value)

先整体看下代码

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  5. boolean evict) {
  6. Node<K,V>[] tab; Node<K,V> p; int n, i;
  7. if ((tab = table) == null || (n = tab.length) == 0)
  8. n = (tab = resize()).length;
  9. if ((p = tab[i = (n - 1) & hash]) == null)
  10. tab[i] = newNode(hash, key, value, null);
  11. else {
  12. Node<K,V> e; K k;
  13. if (p.hash == hash &&
  14. ((k = p.key) == key || (key != null && key.equals(k))))
  15. e = p;
  16. else if (p instanceof TreeNode)
  17. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  18. else {
  19. for (int binCount = 0; ; ++binCount) {
  20. if ((e = p.next) == null) {
  21. p.next = newNode(hash, key, value, null);
  22. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  23. treeifyBin(tab, hash);
  24. break;
  25. }
  26. if (e.hash == hash &&
  27. ((k = e.key) == key || (key != null && key.equals(k))))
  28. break;
  29. p = e;
  30. }
  31. }
  32. if (e != null) { // existing mapping for key
  33. V oldValue = e.value;
  34. if (!onlyIfAbsent || oldValue == null)
  35. e.value = value;
  36. afterNodeAccess(e);
  37. return oldValue;
  38. }
  39. }
  40. ++modCount;
  41. if (++size > threshold)
  42. resize();
  43. afterNodeInsertion(evict);
  44. return null;
  45. }

先看下hash值的计算

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

16 说是为了增加散列度,在 table 的 length 较小的时候,让高位也参与运算,可参考 HashMap计算hashCode时为什么要把高位右移16位

put 的步骤

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],插入节点到末位,判断链表长度是否大于8,大于8的话把链表转换为红黑树;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

计算数组下标

resize() 稍后再看,先看下数组下标计算方法

  1. i = (n - 1) & hash

n 是数组长度,使用hash值取模,这个方法非常巧妙,基于以下公式:x mod 2^n = x & (2^n - 1)。我们知道 HashMap 底层数组的长度总是 2 的 n 次方,并且取模运算为 “h mod table.length”,对应上面的公式,可以得到该运算等同于“h & (table.length - 1)”。这是 HashMap 在速度上的优化,因为 & 比 % 具有更高的效率,为什么 mod 2^n = x & (2^n - 1) 可参见 计算一个数与2的n次方取模

存放数据

计算table下标后,如果table[i] == null,直接新建节点,主要看下非空的情况

  1. key 相同 & equal 比较相等的情况下,直接替换

    1. if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))){
    2. e = p;
    3. }
  2. 判断是否是 TreeNode,即是否是红黑树,如果是红黑树,则直接在树中插入键值对,执行 putTreeVal 方法

    1. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  3. 遍历table[i],插入节点到末位,判断链表长度是否大于8,大于8的话把链表转换为红黑树;遍历过程中若发现key已经存在直接覆盖value即可 ```java for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {

    1. p.next = newNode(hash, key, value, null);
    2. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    3. treeifyBin(tab, hash);
    4. break;

    } if (e.hash == hash &&

    1. ((k = e.key) == key || (key != null && key.equals(k))))
    2. break;

    p = e; }

if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }

  1. <a name="treeifyBin"></a>
  2. #### treeifyBin

/**

  • 将链表节点转为红黑树节点 */ final void treeifyBin(Node[] tab, int hash) { int n, index; Node e; // 1.如果table为空或者table的长度小于64, 调用resize方法进行扩容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    1. resize();
    // 2.根据hash值计算索引值,将该索引位置的节点赋值给e,从e开始遍历该索引位置的链表 else if ((e = tab[index = (n - 1) & hash]) != null) {
    1. TreeNode<K,V> hd = null, tl = null;
    2. do {
    3. // 3.将链表节点转红黑树节点
    4. TreeNode<K,V> p = replacementTreeNode(e, null);
    5. // 4.如果是第一次遍历,将头节点赋值给hd
    6. if (tl == null) // tl为空代表为第一次循环
    7. hd = p;
    8. else {
    9. // 5.如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
    10. p.prev = tl; // 当前节点的prev属性设为上一个节点
    11. tl.next = p; // 上一个节点的next属性设置为当前节点
    12. }
    13. // 6.将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)
    14. tl = p;
    15. } while ((e = e.next) != null);
    16. // 7.将table该索引位置赋值为新转的TreeNode的头节点,如果该节点不为空,则以以头节点(hd)为根节点, 构建红黑树
    17. if ((tab[index] = hd) != null)
    18. hd.treeify(tab);
    } } ```

resize

具体代码参见文末引用

为什么扩容后,节点重 hash 为什么只可能分布在 “原索引位置” 与 “原索引 + oldCap 位置” ?
正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。

hash&(Table.length-1) 就是hash对table长度取模,有两种情况:

  1. hash值是旧table长度的偶数倍(2n)倍 + 取模值,那么table扩容后hash值就是 n 倍table新长度 + 取模值了,还是原来位置
  2. hash 值是旧table长度的奇数(2n + 1)倍 + 取模值,那么table扩容后hash值就是 n 倍table新长度+ 旧table长度 + 取模值

所以有了上面的结论

get(Object key)

  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; Node<K,V> first, e; int n; K k;
  7. // 1.对table进行校验:table不为空 && table长度大于0 &&
  8. // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
  9. if ((tab = table) != null && (n = tab.length) > 0 &&
  10. (first = tab[(n - 1) & hash]) != null) {
  11. // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
  12. if (first.hash == hash && // always check first node
  13. ((k = first.key) == key || (key != null && key.equals(k))))
  14. return first;
  15. // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
  16. if ((e = first.next) != null) {
  17. if (first instanceof TreeNode)
  18. // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
  19. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  20. do {
  21. // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
  22. if (e.hash == hash &&
  23. ((k = e.key) == key || (key != null && key.equals(k))))
  24. return e;
  25. } while ((e = e.next) != null);
  26. }
  27. }
  28. // 6.找不到符合的返回空
  29. return null;

remove(Object key)

  1. /**
  2. * 移除某个节点
  3. */
  4. public V remove(Object key) {
  5. Node<K,V> e;
  6. return (e = removeNode(hash(key), key, null, false, true)) == null ?
  7. null : e.value;
  8. }
  9. final Node<K,V> removeNode(int hash, Object key, Object value,
  10. boolean matchValue, boolean movable) {
  11. Node<K,V>[] tab; Node<K,V> p; int n, index;
  12. // 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
  13. if ((tab = table) != null && (n = tab.length) > 0 &&
  14. (p = tab[index = (n - 1) & hash]) != null) {
  15. Node<K,V> node = null, e; K k; V v;
  16. // 2.如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
  17. if (p.hash == hash &&
  18. ((k = p.key) == key || (key != null && key.equals(k))))
  19. node = p;
  20. else if ((e = p.next) != null) {
  21. // 3.否则将p.next赋值给e,向下遍历节点
  22. // 3.1 如果p是TreeNode则调用红黑树的方法查找节点
  23. if (p instanceof TreeNode)
  24. node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
  25. else {
  26. // 3.2 否则,进行普通链表节点的查找
  27. do {
  28. // 当节点的hash值和key与传入的相同,则该节点即为目标节点
  29. if (e.hash == hash &&
  30. ((k = e.key) == key ||
  31. (key != null && key.equals(k)))) {
  32. node = e; // 赋值给node, 并跳出循环
  33. break;
  34. }
  35. p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
  36. } while ((e = e.next) != null); // e指向下一个节点
  37. }
  38. }
  39. // 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
  40. if (node != null && (!matchValue || (v = node.value) == value ||
  41. (value != null && value.equals(v)))) {
  42. // 4.1 如果是TreeNode则调用红黑树的移除方法
  43. if (node instanceof TreeNode)
  44. ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
  45. // 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,
  46. // “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
  47. else if (node == p)
  48. tab[index] = node.next;
  49. // 4.3 否则将node的上一个节点的next属性设置为node的next节点,
  50. // 即将node节点移除, 将node的上下节点进行关联(链表的移除)
  51. else
  52. p.next = node.next;
  53. ++modCount;
  54. --size;
  55. afterNodeRemoval(node); // 供LinkedHashMap使用
  56. // 5.返回被移除的节点
  57. return node;
  58. }
  59. }
  60. return null;
  61. }

死循环问题

参考文末引用

HashMap 和 Hashtable 的区别

  • HashMap 允许 key 和 value 为 null,Hashtable 不允许。
  • HashMap 的默认初始容量为 16,Hashtable 为 11。
  • HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
  • HashMap 是非线程安全的,Hashtable是线程安全的。
  • HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
  • HashMap 去掉了 Hashtable 中的 contains 方法。
  • HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。

参考 史上最详细的 JDK 1.8 HashMap 源码解析