1. 源码详解

1.0. 部分参数和源码

1.0.1. 部分参数

参数 参数含义 大小
DEFAULT_INITIAL_CAPACITY 默认初始散列表大小 1<<4 (16)
DEFAULT_LOAD_FACTOR 默认负载因数 0.75
TREEIFY_THRESHOLD 红黑树化阈值 8
MIN_TREEIFY_CAPACITY 红黑树化的最小散列表大小 64

1.0.2. putVal()源码 + 注释

put一个元素的时候调用该方法。

  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
  2. Node<K,V>[] tab; Node<K,V> p; int n, i;
  3. //如果散列表为空,则用resize()创建数组
  4. if ((tab = table) == null || (n = tab.length) == 0)
  5. n = (tab = resize()).length;
  6. //如果散列表的当前位置为空,则以该插入元素创建新节点插入到该数组位置
  7. //p为数组当前位置的元素节点
  8. if ((p = tab[i = (n - 1) & hash]) == null)
  9. tab[i] = newNode(hash, key, value, null);
  10. //如果数组当前位置不为空
  11. else {
  12. Node<K,V> e; K k;
  13. //如果插入元素的key和数组当前位置元素key相同,将e指向当前位置元素
  14. if (p.hash == hash &&
  15. ((k = p.key) == key || (key != null && key.equals(k))))
  16. e = p;
  17. //如果数组中当前位置为红黑树
  18. else if (p instanceof TreeNode)
  19. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  20. //如果数组中当前位置为链表
  21. else {
  22. //遍历链表元素,并进行累加
  23. for (int binCount = 0; ; ++binCount) {
  24. //如果找到链表末端也没有找到该key,则将KV生成节点,并插入到链表中
  25. //判断链表长度,如果链表已存在链表长度阈值(8)个元素,即插入元素为第阈值+1(9)个元素,将链表转化为红黑树
  26. if ((e = p.next) == null) {
  27. p.next = newNode(hash, key, value, null);
  28. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  29. treeifyBin(tab, hash);
  30. break;
  31. }
  32. //如果链表中找到相同key,则直接跳出循环
  33. if (e.hash == hash &&
  34. ((k = e.key) == key || (key != null && key.equals(k))))
  35. break;
  36. //每次循环后将p指向p.next(e=p.next)
  37. p = e;
  38. }
  39. }
  40. //如果e不为null(说明hm中已经存在该key,则进行value修改)
  41. if (e != null) { // existing mapping for key
  42. V oldValue = e.value;
  43. //onlyIfAbsent为true时不改变已存在的value,为false时进行更新,默认为false
  44. if (!onlyIfAbsent || oldValue == null)
  45. e.value = value;
  46. //空函数【Callback to allow LinkedHashMap post-actions】
  47. afterNodeAccess(e);
  48. return oldValue;
  49. }
  50. }
  51. ++modCount;
  52. //如果数组长度超过阈值则进行扩容
  53. if (++size > threshold)
  54. resize();
  55. afterNodeInsertion(evict);
  56. return null;
  57. }

1.0.3. treeifyBin() 源码 + 注释

判断散列表是否满足最小树化容量,如果不满足进行resize()扩容;
如果满足,将单链表转换为双向链表;
调用treeify()方法对双向链表进行红黑树化。

  1. final void treeifyBin(Node<K,V>[] tab, int hash) {
  2. int n, index; Node<K,V> e;
  3. //判断,如果散列表为空或散列表的容量小于最小树化容量,则不会进行树化,而是对散列表进行扩容
  4. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
  5. resize();
  6. //如果该hash值在散列表对应位置的Node元素不为null
  7. else if ((e = tab[index = (n - 1) & hash]) != null) {
  8. TreeNode<K,V> hd = null, tl = null;
  9. //遍历链表,先将单链表转换为双向链表
  10. do {
  11. //在Node对象的基础上创建TreeNode对象
  12. TreeNode<K,V> p = replacementTreeNode(e, null);
  13. if (tl == null)
  14. hd = p;
  15. else {
  16. p.prev = tl;
  17. tl.next = p;
  18. }
  19. tl = p;
  20. } while ((e = e.next) != null);
  21. if ((tab[index] = hd) != null)
  22. hd.treeify(tab);
  23. }
  24. }

1.0.4. resize() 源码 + 注释

  1. final Node<K,V>[] resize() {
  2. //指向当前散列表
  3. Node<K,V>[] oldTab = table;
  4. //旧散列表的容量
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6. //旧散列表的阈值
  7. int oldThr = threshold;
  8. int newCap, newThr = 0;
  9. //如果旧散列表已存在
  10. if (oldCap > 0) {
  11. //如果旧散列表容量达到上限,则不对其进行扩容,同时将扩容阈值调整大Integer的最大值
  12. if (oldCap >= MAXIMUM_CAPACITY) {
  13. threshold = Integer.MAX_VALUE;
  14. return oldTab;
  15. }
  16. //如果旧散列表可以进行扩容(旧容量小于上限的一半),且旧容量大于等于初始值,则对容量和扩容阈值进行扩大(旧值乘2)
  17. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  18. oldCap >= DEFAULT_INITIAL_CAPACITY)
  19. newThr = oldThr << 1; // double threshold
  20. }
  21. //如果旧散列表不存在,但扩容阈值存在,则将新容量设置为旧扩容阈值
  22. else if (oldThr > 0) // initial capacity was placed in threshold
  23. newCap = oldThr;
  24. //如果这是一个纯净的HashMap对象(啥都没),则设置新容量和新扩容阈值为默认值
  25. else { // zero initial threshold signifies using defaults
  26. newCap = DEFAULT_INITIAL_CAPACITY;
  27. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  28. }
  29. //如果新扩容阈值为0,则根据新容量和负载因子进行计算
  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;
  36. @SuppressWarnings({"rawtypes","unchecked"})
  37. //创建一个大小为新容量的散列表
  38. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  39. table = newTab;
  40. if (oldTab != null) {
  41. //遍历旧散列表中的元素
  42. for (int j = 0; j < oldCap; ++j) {
  43. Node<K,V> e;
  44. //如果元素不为空
  45. if ((e = oldTab[j]) != null) {
  46. //将旧散列表中该位置置空
  47. oldTab[j] = null;
  48. //如果该位置存储的是单个元素
  49. if (e.next == null)
  50. newTab[e.hash & (newCap - 1)] = e;
  51. //如果该位置存储的是红黑树
  52. else if (e instanceof TreeNode)
  53. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  54. //如果该位置存储的是链表
  55. else { // preserve order
  56. //设置低位和高位的头尾,扩容前元素位于散列表的 index 位置,扩容后元素位于散列表的index和(index+oldCap)位置
  57. Node<K,V> loHead = null, loTail = null;
  58. Node<K,V> hiHead = null, hiTail = null;
  59. Node<K,V> next;
  60. //这里将一个链表拆成两个链表
  61. do {
  62. next = e.next;
  63. if ((e.hash & oldCap) == 0) {
  64. if (loTail == null)
  65. loHead = e;
  66. else
  67. loTail.next = e;
  68. loTail = e;
  69. }
  70. else {
  71. if (hiTail == null)
  72. hiHead = e;
  73. else
  74. hiTail.next = e;
  75. hiTail = e;
  76. }
  77. } while ((e = next) != null);
  78. //将链表分别插入
  79. if (loTail != null) {
  80. loTail.next = null;
  81. newTab[j] = loHead;
  82. }
  83. if (hiTail != null) {
  84. hiTail.next = null;
  85. newTab[j + oldCap] = hiHead;
  86. }
  87. }
  88. }
  89. }
  90. }
  91. return newTab;
  92. }

1.0.5. treeify() 源码 + 注释

将双向链表转换为红黑树。

/**
         * Forms tree of the nodes linked from this node.
         */
        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                //如果根节点为空
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                //如果根节点不为空
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        //根据hash值的大小判断方向
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

1.0.6. getNode() 源码 + 注释

获取节点。

/**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判断散列表存在,不为空,hash值对应的位置不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //判断散列表当前位置元素是否与查找元素碰撞
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //判断是否有下一个元素
            if ((e = first.next) != null) {
                //如果是红黑树,在树中查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //是链表的话就遍历查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

1.1 HashMap对象创建流程

空参构造:默认初始大小为16,在第一次调用resize()方法时创建数组;
有参构造:传入的初始容量值X会经过一系列运算转换为一个大于等于X且为2的幂次方的数,作为数组的初始大小,在第一次调用resize()方法时创建数组。

int cap = 10;
int n = cap - 1;
n |= n >>> 1; //  n |= (n>>>1)  2^0
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;

System.out.println(n+1);//输出为16

1.2. put流程

1.2.1. 生成key的HashCode值:

调用函数的hash()方法;
调用key的hashCode()方法;
将该值右移16位;
将两者取异或。
如果key为null,hashCode为0。

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

1.2.2. 校验散列表是否有值:

如果散列表table为空,则调用resize()方法初始化散列表,计算散列表扩容阈值threshold。

int newThr = 0;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
threshold = newThr;

1.2.3. 计算该hashCode值位于散列表中的位置:

如果散列表当前位置为空,则以k,v值和hash值创建Node对象,next设置为null,跳转到1.1.5.;

tab[i] = newNode(hash, key, value, null);

如果散列表当前位置不为空,则进行三种情况判断,见1.1.4.。

1.2.4. 对散列表当前位置不为空进行判断:

1.如果散列表中当前位置元素的key与插入元素的key相同,则修改value,并返回该key的oldValue;
2.如果散列表中当前位置存储的是红黑树(p instanceof TreeNode),则调用putTreeVal()方法向红黑树中插入元素或修改红黑树中元素的value;
3.如果散列表中当前位置存储的是链表,则遍历链表元素并对元素个数累加(++binCount)(**这里的binCount严格的说并不是元素个数,而是元素个数-1,或者说是链表尾部元素的索引)
3.1. 如果搜索到链表的尾部也没有找到该key,则将该值插入到链表尾部,
(新插入的元素是不计算在binCount中的)
3.1.1. 此时如果binCount>=树化阈值-1,则会将调用treeifyBin()方法;
3.1.1.1. 如果此时散列表长度小于最小树化容量,则会对该散列表
进行扩容**,而不进行树化;
3.1.1.2. 如果此时散列表长度大于等于最小树化容量,则会将该单向链表转换为双向链表并调用treeify()方法对其进行红黑树化。

if(binCount >= TREEIFY_THRESHOLD -1)
    treeifyBin(tab,hash);

3.2. 如果在链表中找到与插入元素相同的key,则修改value,并返回该key的oldValue;

1.2.5. 判断散列表是否需要进行扩容

对散列表的空位置插入元素后会对全局变量 size 进行递增,判断递增后的size是否大于扩容阈值(threshold),如果需要扩容,跳转到1.2.6.。

1.2.6. 对散列表进行扩容

一般是将散列表的容量以及散列表的扩容阈值各增大一倍。
对于旧散列表中的旧链表,让其中对象的hash值和旧链表的长度取&,获得0或非0。根据0,非0将旧的链表拆分成两个新的链表,然后再插入到新散列表中;
插入的新位置分别是:index = hashCode & (oldCap-1) 和 index+oldCap;
对于旧散列表中的旧红黑树,由于依然保持了节点间的链表关系,所以在扩容时的拆分实际上是对链表进行遍历拆分。和对链表拆分一样,根据0或非0拆分成两个部分,如果其中一个部分长度不足,则会将TreeNode退化成Node。

1.3.get流程

1.3.1. 生成key的hashCode

1.3.2. 校验散列表

如果散列表不存在或为空或hash值对应的位置为空,则直接返回null;
如果散列表存在,不为空,hash值对应的位置不为空,则进行下一步判断;

1.3.2. 校验对应位置元素是否碰撞

if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
    return first;

如果当前位置元素(first)的hash值、key与被查找key的hash值和其本身相同,则返回该元素;(&& 后面的部分用了或判断,是为了区别key为Integer类型和String类型这两种情况)
否则进行下一步判断;

1.3.3. 判断当前位置元素是否有next

如果有next,判断first对象是否为TreeNode类型
如果是TreeNode类型,则调用getTreeNode()方法;
如果不是TreeNode类型,则是链表元素,进行链表遍历;
找到则返回元素,没找到则返回空。

2.面试问题总结

0.什么是Hash冲突

通过put()向HashMap中添加元素时计算该元素对应的数组索引,插入元素的时候发现该bucket中已经有元素了,这个时候就产生了Hash冲突。

1.谈一下HashMap的特性

  1. HashMap存储键值对,可以实现快速存取,允许key为null,不允许key重复,若key重复则用新value覆盖旧value。
  2. 非同步,线程不安全
  3. 底层是hash表,不保证插入读取有序。

2.HashMap的底层原理是什么?

从几个方面说:1.key的hashCode值
2.数据结构
3.存取

  1. 对于每一个key,会调用hash(key)方法计算出一个与之对应且唯一的hashCode值;
  2. HashMap的底层结构是数组加链表,当某个链表的长度达到8(或者9)并且当数组的长度达到64时会对该链表进行红黑树化;
  3. put元素时会根据key的hashCode计算出元素在数组的index,然后尝试将该元素封装成Node或TreeNode对象插入到数组或数组中的链表或数组中的红黑树中;
  4. 当数组存储元素的个数达到数组扩容阈值时会对数组进行扩容,当某个链表的长度达到9,并且数组长度没有达到64时也会对数组进行扩容;
  5. 数组的扩容是将数组原来的容量乘2,将原来的扩容阈值乘2,同时将旧链表中的链表和红黑树拆分;
  6. get(key)时会根据key 的hashCode计算出其在数组的下标,然后通过==或equals()方法在数组或链表或红黑树中找到相同的key,如果找不到就返回null,找到了就返回对应的value。

3.谈一下HashMap中put是如何实现的

  1. 根据key生成hashCode;
  2. 判断当前HashMap对象中的数组是否为空,如果为空则调用resize()方法初始化该数组;
  3. 将hashCode值与(数组长度-1)进行与运算,算出hashcode基于当前数组对应的数组下标i;
  4. 判断数组的第i个位置的元素(tab[i])是否为空;
    1. 如果为空,则将key,value封装为Node对象赋值给tab[i];
    2. 如果不为空:
      1. 如果put方法传入进来的key等于tab[i].key(key的hash值相同且长得也一样),那么证明存在相同的key;
      2. 如果不等于tab[i].key,则:
        1. 如果tab[i]的类型是TreeNode,则表示数组的第i位置上是一颗红黑树,判断在红黑树中是否存在相同的key,如果没有相同key,那么将key和value封装成TreeNode对象插入到红黑树中,同时要生成一个Node对象维护红黑树对象间的链表关系;
        2. 如果tab[i]的类型不是TreeNode,则表示数组的第i位置上是一个链表,那么遍历链表寻找是否存在相同的key,并且在遍历的过程中会对链表中的结点数进行计数,当遍历到最后一个结点时,会将key,value封装为Node插入到链表的尾部,同时判断在插入新结点之前的链表结点个数是不是大于等于8,如果是,则将链表改为红黑树;
      3. 如果上述步骤中发现存在相同的key,则根据onlyIfAbsent标记来判断是否需要更新value值,然后返回oldValue
  5. modCount++
  6. HashMap的元素个数size加1
  7. 如果size大于扩容的阈值,则进行扩容

4.谈一下HashMap中什么时候需要进行扩容,resize()是如何实现的?

扩容是resize()方法实现的,resize()方法主要有两个作用,创建数组和扩容;
扩容的时机有两个:
1.一个是数组中的链表长度达到9(或者说对链表长度计数的binCount达到8)但是数组大小没有达到64时,会对数组进行扩容;
2.另一个是数组中存储元素的个数达到数组扩容阈值时,会对数组进行扩容;
resize()的实现:
1.resize()会先校验数组,如果数组不存在或为空,会对数组进行初始化;
2.如果数组存在且不为空会生成一个新数组,大小为原来的两倍,同时更新扩容阈值为原阈值的两倍;
3.然后遍历数组中的元素,将该元素从旧数组中移除:
i:如果是单个元素,则将其放入新的列表中 index =(hashcode & (newCap -1));
ii:如果是链表,拆分成两个链表分别加入到新列表中;
iii:如果是红黑树,拆分成两个链表,长度足够的进行树化,长度不够的退化成链表,分别加入到新列表中;

5.谈一下HashMap中get()是如何实现的?

  1. 根据key生成hashcode;
  2. 如果数组为空,则直接返回空;
  3. 如果数组不为空,将hashCode值与(数组长度-1)进行与运算,算出hashcode基于当前数组对应的数组下标i;
  4. 如果数组的第i个位置上没有元素,则直接返回空;
  5. 如果数组的第1个位上的Node元素的hash与目标Node的hash相同并且两者Node.key==或equals,则返回该元素,并获取该元素的value;
  6. 如果不等于则判断该元素还有没有下一个元素,如果没有,返回空;
  7. 如果有则判断该元素的类型是链表结点还是红黑树结点:
    1. 如果是链表则遍历链表
    2. 如果是红黑树则遍历红黑树
  8. 找到即返回元素,没找到的则返回空。

6.谈一下HashMap中hash函数是怎么实现的?

取key的hashCode()方法的返回值,与其高16位做异或运算;

7.为什么不直接将key作为哈希值而是与高16位做异或运算?

增加key在数组中索引的随机性,减少hash冲突:
将hash值与其高16位做异或运算是为了在计算key在数组中索引时更好的保留高16位的特征。数组的初始长度为16,计算key在数组中索引时,会用 hashCode & (n-1) ,此时 hashCode与低四位(00..0 1111)做与运算,很容易得到相同的索引。同样的,当数组长度比较小时,高16位都为0,如果将hash值与其高16位做异或可以将高位低位相结合,增加了随机性。
使用异或运算的原因是:异或运算能更好的保留各部分的特征,如果采用 & 运算,计算出来的值会向0靠拢;如果采用 | 运算,计算出来的值会向1靠拢。

8.为什么数组默认长度是16,为什么容量必须是2的幂?如果手动设置的初始值不是2的幂会如何?

为了减少hash冲突。因为计算索引使用的是 (hash值 &( cap - 1)),如果容量不是2的幂会导致索引分布严重不均,数据产生倾斜;
如果手动设置的初始值不是2的幂,会通过算法将初始值变为大于手动设置值的最小2的幂。例如设置10,初始容量会计算得到16;

9.当两个对象的hashCode相等时会怎样?

如果两个对象的key也相同,会用新value替换旧value;
如果两个对象的key不相同,会将新的对象插入到链表后面,
如果插入前链表的长度已经达到8且数组长度大于等于64则转化为红黑树,
如果插入前链表的长度已经达到8且数组长度小于64则对HashMap进行扩容。

10.如果两个键的hashCode相同,会如何获取值对象?

会通过equals比较内容获取相同key的对象,然后获取对象的值。

11.如果HashMap的大小超过了负载因子(loadFactor)会如何?

如果HashMap的大小没有达到上限,会对HashMap进行扩容以及对链表和红黑树进行拆分操作,并调整扩容阈值为原来两倍;
如果HashMap的大小已经达到上限,不会对HashMap进行扩容,但会将扩容阈值调整到Integer.MAX_VALUE。

12.HashMap和HashTable的区别?

相同点:都是存储key-value键值对的数据结构。
不同点:

  1. key的允许范围不同:HashMap允许Key-value为null,HashTable不允许;
  2. 线程安全不同:hashMap没有考虑同步,是线程不安全的。HashTable是线程安全的,给api套上了一层synchronized修饰;
  3. 继承类不同:HashMap继承于AbstractMap类,HashTable继承与Dictionary类。
  4. 迭代器(Iterator)不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException。
  5. 容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为”原始容量x2”。Hashtable默认的容量大小是11;增加容量时,每次将容量变为”原始容量x2 + 1”;
  6. 添加key-value时的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法。HashTable没有自定义哈希算法,而直接采用的key的hashCode()。

13.解释HashMap的参数loadFactor,作用是什么?

负载因子可以理解为散列表的拥挤程度,影响hash操作到同一个bucket的概率。默认的loadFactor大小为0.75,表示当HashMap中数组容纳元素的数量达到最大长度的75%时,需要对HashMap进行扩容,在HashMap的构造器中可以自定义loadFactor。loadFactor不宜过大或过小,过小的负载因子虽然可以降低hash冲突的发生,但是会使扩容频率太高,浪费空间;过大的负载因子提高了hash冲突发生的可能性。

14.传统HashMap的缺点(为什么引入红黑树?)

传统HashMap(JDK1.8之前)的实现是用数组+链表,当大量的元素都放到一个桶中时会产生很长的链表,这个时候HashMap就类似于退化成了单链表,遍历的时间为O(n),失去了优势。引入红黑树可以让很长的链表红黑树化,查找的时间复杂度为O(logn),优化了速度。

15.使用HashMap时一般用什么类型的元素作为key?

Integer,String这种不可变类型
像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的。

16. 为什么HashMap.TreeNode继承于LinkedHashMap.Entry

首先说一下继承与实现结构:
HashMap.Node 实现了 Map.Entry 接口;
LinkedHashMap.Entry 继承了 HashMap.Node,并增加了 before** after 两个指针;
HashMap.TreeNode 继承了 LinkedHashMap.Entry,并增加了
parentleftrightprev** 这四个指针 和一个 red boolean变量;

由此见得,这种继承方式当在HashMap中使用TreeNode时,会有beforeafter两个多余指针;
而如果是 LinkedHashMap.Entry 继承于 HashMap.TreeNode,在LinkedHashMap中使用Entry时,会有parentleftrightprev这四个多余指针,以及red这个多余变量。
两相比较下,当前的继承方式更好。

17. 为什么重写equals()方法后一定要重写hashCode()方法

equals和hashCode()是Object的方法。
equals默认比较两个对象的内存地址是否相同( == )。
hashcode是根据对象的内存地址经哈希算法计算出来的值。
如果不重写hashCode()方法,当我们将某个自定义类对象作为key向map中放入时,会导致有参数完全相同的对象被存放到不到的bucket。