1 put过程

  1. /**
  2. * Implements Map.put and related methods.
  3. *
  4. * @param hash hash for key
  5. * @param key the key
  6. * @param value the value to put
  7. * @param onlyIfAbsent if true, don't change existing value
  8. * @param evict if false, the table is in creation mode.
  9. * @return previous value, or null if none
  10. */
  11. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  12. boolean evict) {
  13. Node<K,V>[] tab; Node<K,V> p; int n, i;
  14. if ((tab = table) == null || (n = tab.length) == 0)
  15. n = (tab = resize()).length;
  16. if ((p = tab[i = (n - 1) & hash]) == null)
  17. tab[i] = newNode(hash, key, value, null);
  18. else {
  19. Node<K,V> e; K k;
  20. if (p.hash == hash &&
  21. ((k = p.key) == key || (key != null && key.equals(k))))
  22. e = p;
  23. else if (p instanceof TreeNode)
  24. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  25. else {
  26. for (int binCount = 0; ; ++binCount) {
  27. if ((e = p.next) == null) {
  28. p.next = newNode(hash, key, value, null);
  29. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  30. treeifyBin(tab, hash);
  31. break;
  32. }
  33. if (e.hash == hash &&
  34. ((k = e.key) == key || (key != null && key.equals(k))))
  35. break;
  36. p = e;
  37. }
  38. }
  39. if (e != null) { // existing mapping for key
  40. V oldValue = e.value;
  41. if (!onlyIfAbsent || oldValue == null)
  42. e.value = value;
  43. afterNodeAccess(e);
  44. return oldValue;
  45. }
  46. }
  47. ++modCount;
  48. if (++size > threshold)
  49. resize();
  50. afterNodeInsertion(evict);
  51. return null;
  52. }

hashMap - 图1

1.判断散列表是否为null,如果为null,重新初始化散列表
2.如果不为null并且没有发生碰撞,直接添加元素到散列表中
3.如果发生碰撞了,并且要插入的元素的桶的hash和key都相等,记录下来,将新值覆盖旧值
4.如果桶hash与key不想等,并且该节点是红黑树结构,调用树的插入方法
5.如果是链表结构,找到了key映射节点,就记录这个节点,退出循环。如果没有找到,在链表尾部插入节点。
插入后如果发现临界值大于TREEIFY_THRESHOLD,转成红黑树

2 解决hash冲突的一般过程

  1. 开放地址法:此处不留爷,我去下一家
  2. 分离链表法:就像窗口打饭,来一个打饭的,跟在后面就行了,构成链表

    3 hashMap扩容阈值为何是2的整数幂?

    ~~ 初始容量为何是16
    扩容代码:
    1. /**
    2. * Initializes or doubles table size. If null, allocates in
    3. * accord with initial capacity target held in field threshold.
    4. * Otherwise, because we are using power-of-two expansion, the
    5. * elements from each bin must either stay at same index, or move
    6. * with a power of two offset in the new table.
    7. *
    8. * @return the table
    9. */
    10. final Node<K,V>[] resize() {
    11. Node<K,V>[] oldTab = table;
    12. int oldCap = (oldTab == null) ? 0 : oldTab.length;
    13. int oldThr = threshold;
    14. int newCap, newThr = 0;
    15. if (oldCap > 0) {
    16. if (oldCap >= MAXIMUM_CAPACITY) {
    17. threshold = Integer.MAX_VALUE;
    18. return oldTab;
    19. }
    20. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    21. oldCap >= DEFAULT_INITIAL_CAPACITY)
    22. newThr = oldThr << 1; // double threshold
    23. }
    24. else if (oldThr > 0) // initial capacity was placed in threshold
    25. newCap = oldThr;
    26. else { // zero initial threshold signifies using defaults
    27. newCap = DEFAULT_INITIAL_CAPACITY;
    28. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    29. }
    30. if (newThr == 0) {
    31. float ft = (float)newCap * loadFactor;
    32. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    33. (int)ft : Integer.MAX_VALUE);
    34. }
    35. threshold = newThr; //threshold:扩容阈值loadFactor(0.75)*length(16)
    36. @SuppressWarnings({"rawtypes","unchecked"})
    37. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    38. table = newTab;
    39. if (oldTab != null) {
    40. for (int j = 0; j < oldCap; ++j) {
    41. Node<K,V> e;
    42. if ((e = oldTab[j]) != null) {
    43. oldTab[j] = null;
    44. if (e.next == null)
    45. newTab[e.hash & (newCap - 1)] = e;
    46. else if (e instanceof TreeNode)
    47. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
    48. else { // preserve order
    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. if ((e.hash & oldCap) == 0) {
    55. if (loTail == null)
    56. loHead = e;
    57. else
    58. loTail.next = e;
    59. loTail = e;
    60. }
    61. else {
    62. if (hiTail == null)
    63. hiHead = e;
    64. else
    65. hiTail.next = e;
    66. hiTail = e;
    67. }
    68. } while ((e = next) != null);
    69. if (loTail != null) {
    70. loTail.next = null;
    71. newTab[j] = loHead;
    72. }
    73. if (hiTail != null) {
    74. hiTail.next = null;
    75. newTab[j + oldCap] = hiHead;
    76. }
    77. }
    78. }
    79. }
    80. }
    81. return newTab;
    82. }

容量计算方法:

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. ......
  3. this.threshold = tableSizeFor(initialCapacity);
  4. }
  5. static final int tableSizeFor(int cap) {
  6. int n = cap - 1;
  7. n |= n >>> 1;
  8. n |= n >>> 2;
  9. n |= n >>> 4;
  10. n |= n >>> 8;
  11. n |= n >>> 16;
  12. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  13. }

总结:为什么这里一定要指定容量为2的n次方呢?
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。这样也能使hashcode的取模结果更为平均,尽量的减少冲突
下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

hashMap - 图2

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
hashMap - 图3

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

4 为什么在链表长度为 8 的时候转红黑树,为啥不能是 9 是 10?

  1. * threshold of 0.75, although with a large variance because of
  2. * resizing granularity. Ignoring variance, the expected
  3. * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
  4. * factorial(k)). The first values are:
  5. *
  6. * 0: 0.60653066
  7. * 1: 0.30326533
  8. * 2: 0.07581633
  9. * 3: 0.01263606
  10. * 4: 0.00157952
  11. * 5: 0.00015795
  12. * 6: 0.00001316
  13. * 7: 0.00000094
  14. * 8: 0.00000006
  15. * more: less than 1 in ten million

总结:随机hash码下,哈希表中频率分布遵循泊松分布(单位(时间)内出现的次数),长度为8,出现的概率为千万分之6,概率极低

5 为什么要转红黑树?

转红黑树代码:

  1. /**
  2. * Replaces all linked nodes in bin at index for given hash unless
  3. * table is too small, in which case resizes instead.
  4. */
  5. final void treeifyBin(Node<K,V>[] tab, int hash) {
  6. int n, index; Node<K,V> e;
  7. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
  8. resize();
  9. ......
  10. }

问题博客
总结:① 在获取数据的时候,链表复杂度为O(n),而链表复杂度为O(logN)
② 为何不直接使用红黑树,因为树节点的大小是链表节点大小的两倍,所以只有在容器中包含足够的节点保证使用才用它。显然尽管转为树使得查找的速度更快,但是在节点数比较小的时候,此时对于红黑树来说内存上的劣势会

6 JDK1.8做了那些改变?

  1. hash表由原来的“数组+链表”——>”数组+链表+红黑树”
  2. 链表增加节点“头插法”——>”尾插法”(避免死锁)

参考文档

美团HashMap
语雀:hashMap就这么简单