- 1. 源码详解
- 2.面试问题总结
- 0.什么是Hash冲突
- 1.谈一下HashMap的特性
- 2.HashMap的底层原理是什么?
- 3.谈一下HashMap中put是如何实现的
- 4.谈一下HashMap中什么时候需要进行扩容,resize()是如何实现的?
- 5.谈一下HashMap中get()是如何实现的?
- 6.谈一下HashMap中hash函数是怎么实现的?
- 7.为什么不直接将key作为哈希值而是与高16位做异或运算?
- 8.为什么数组默认长度是16,为什么容量必须是2的幂?如果手动设置的初始值不是2的幂会如何?
- 9.当两个对象的hashCode相等时会怎样?
- 10.如果两个键的hashCode相同,会如何获取值对象?
- 11.如果HashMap的大小超过了负载因子(loadFactor)会如何?
- 12.HashMap和HashTable的区别?
- 13.解释HashMap的参数loadFactor,作用是什么?
- 14.传统HashMap的缺点(为什么引入红黑树?)
- 15.使用HashMap时一般用什么类型的元素作为key?
- 16. 为什么HashMap.TreeNode继承于LinkedHashMap.Entry
- 17. 为什么重写equals()方法后一定要重写hashCode()方法
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一个元素的时候调用该方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果散列表为空,则用resize()创建数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果散列表的当前位置为空,则以该插入元素创建新节点插入到该数组位置
//p为数组当前位置的元素节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果数组当前位置不为空
else {
Node<K,V> e; K k;
//如果插入元素的key和数组当前位置元素key相同,将e指向当前位置元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果数组中当前位置为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果数组中当前位置为链表
else {
//遍历链表元素,并进行累加
for (int binCount = 0; ; ++binCount) {
//如果找到链表末端也没有找到该key,则将KV生成节点,并插入到链表中
//判断链表长度,如果链表已存在链表长度阈值(8)个元素,即插入元素为第阈值+1(9)个元素,将链表转化为红黑树
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果链表中找到相同key,则直接跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//每次循环后将p指向p.next(e=p.next)
p = e;
}
}
//如果e不为null(说明hm中已经存在该key,则进行value修改)
if (e != null) { // existing mapping for key
V oldValue = e.value;
//onlyIfAbsent为true时不改变已存在的value,为false时进行更新,默认为false
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空函数【Callback to allow LinkedHashMap post-actions】
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果数组长度超过阈值则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
1.0.3. treeifyBin() 源码 + 注释
判断散列表是否满足最小树化容量,如果不满足进行resize()扩容;
如果满足,将单链表转换为双向链表;
调用treeify()方法对双向链表进行红黑树化。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//判断,如果散列表为空或散列表的容量小于最小树化容量,则不会进行树化,而是对散列表进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//如果该hash值在散列表对应位置的Node元素不为null
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
//遍历链表,先将单链表转换为双向链表
do {
//在Node对象的基础上创建TreeNode对象
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
1.0.4. resize() 源码 + 注释
final Node<K,V>[] resize() {
//指向当前散列表
Node<K,V>[] oldTab = table;
//旧散列表的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧散列表的阈值
int oldThr = threshold;
int newCap, newThr = 0;
//如果旧散列表已存在
if (oldCap > 0) {
//如果旧散列表容量达到上限,则不对其进行扩容,同时将扩容阈值调整大Integer的最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果旧散列表可以进行扩容(旧容量小于上限的一半),且旧容量大于等于初始值,则对容量和扩容阈值进行扩大(旧值乘2)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果旧散列表不存在,但扩容阈值存在,则将新容量设置为旧扩容阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果这是一个纯净的HashMap对象(啥都没),则设置新容量和新扩容阈值为默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新扩容阈值为0,则根据新容量和负载因子进行计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个大小为新容量的散列表
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍历旧散列表中的元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果元素不为空
if ((e = oldTab[j]) != null) {
//将旧散列表中该位置置空
oldTab[j] = null;
//如果该位置存储的是单个元素
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果该位置存储的是红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果该位置存储的是链表
else { // preserve order
//设置低位和高位的头尾,扩容前元素位于散列表的 index 位置,扩容后元素位于散列表的index和(index+oldCap)位置
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;
}
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的特性
- HashMap存储键值对,可以实现快速存取,允许key为null,不允许key重复,若key重复则用新value覆盖旧value。
- 非同步,线程不安全
- 底层是hash表,不保证插入读取有序。
2.HashMap的底层原理是什么?
从几个方面说:1.key的hashCode值
2.数据结构
3.存取
- 对于每一个key,会调用hash(key)方法计算出一个与之对应且唯一的hashCode值;
- HashMap的底层结构是数组加链表,当某个链表的长度达到8(或者9)并且当数组的长度达到64时会对该链表进行红黑树化;
- put元素时会根据key的hashCode计算出元素在数组的index,然后尝试将该元素封装成Node或TreeNode对象插入到数组或数组中的链表或数组中的红黑树中;
- 当数组存储元素的个数达到数组扩容阈值时会对数组进行扩容,当某个链表的长度达到9,并且数组长度没有达到64时也会对数组进行扩容;
- 数组的扩容是将数组原来的容量乘2,将原来的扩容阈值乘2,同时将旧链表中的链表和红黑树拆分;
- get(key)时会根据key 的hashCode计算出其在数组的下标,然后通过==或equals()方法在数组或链表或红黑树中找到相同的key,如果找不到就返回null,找到了就返回对应的value。
3.谈一下HashMap中put是如何实现的
- 根据key生成hashCode;
- 判断当前HashMap对象中的数组是否为空,如果为空则调用resize()方法初始化该数组;
- 将hashCode值与(数组长度-1)进行与运算,算出hashcode基于当前数组对应的数组下标i;
- 判断数组的第i个位置的元素(tab[i])是否为空;
- 如果为空,则将key,value封装为Node对象赋值给tab[i];
- 如果不为空:
- 如果put方法传入进来的key等于tab[i].key(key的hash值相同且长得也一样),那么证明存在相同的key;
- 如果不等于tab[i].key,则:
- 如果tab[i]的类型是TreeNode,则表示数组的第i位置上是一颗红黑树,判断在红黑树中是否存在相同的key,如果没有相同key,那么将key和value封装成TreeNode对象插入到红黑树中,同时要生成一个Node对象维护红黑树对象间的链表关系;
- 如果tab[i]的类型不是TreeNode,则表示数组的第i位置上是一个链表,那么遍历链表寻找是否存在相同的key,并且在遍历的过程中会对链表中的结点数进行计数,当遍历到最后一个结点时,会将key,value封装为Node插入到链表的尾部,同时判断在插入新结点之前的链表结点个数是不是大于等于8,如果是,则将链表改为红黑树;
- 如果上述步骤中发现存在相同的key,则根据onlyIfAbsent标记来判断是否需要更新value值,然后返回oldValue
- modCount++
- HashMap的元素个数size加1
- 如果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()是如何实现的?
- 根据key生成hashcode;
- 如果数组为空,则直接返回空;
- 如果数组不为空,将hashCode值与(数组长度-1)进行与运算,算出hashcode基于当前数组对应的数组下标i;
- 如果数组的第i个位置上没有元素,则直接返回空;
- 如果数组的第1个位上的Node元素的hash与目标Node的hash相同并且两者Node.key==或equals,则返回该元素,并获取该元素的value;
- 如果不等于则判断该元素还有没有下一个元素,如果没有,返回空;
- 如果有则判断该元素的类型是链表结点还是红黑树结点:
- 如果是链表则遍历链表
- 如果是红黑树则遍历红黑树
- 找到即返回元素,没找到的则返回空。
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键值对的数据结构。
不同点:
- key的允许范围不同:HashMap允许Key-value为null,HashTable不允许;
- 线程安全不同:hashMap没有考虑同步,是线程不安全的。HashTable是线程安全的,给api套上了一层synchronized修饰;
- 继承类不同:HashMap继承于AbstractMap类,HashTable继承与Dictionary类。
- 迭代器(Iterator)不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException。
- 容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为”原始容量x2”。Hashtable默认的容量大小是11;增加容量时,每次将容量变为”原始容量x2 + 1”;
- 添加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,并增加了 parent、left、right、prev** 这四个指针 和一个 red boolean变量;
由此见得,这种继承方式当在HashMap中使用TreeNode时,会有before和after两个多余指针;
而如果是 LinkedHashMap.Entry 继承于 HashMap.TreeNode,在LinkedHashMap中使用Entry时,会有parent、left、right、prev这四个多余指针,以及red这个多余变量。
两相比较下,当前的继承方式更好。
17. 为什么重写equals()方法后一定要重写hashCode()方法
equals和hashCode()是Object的方法。
equals默认比较两个对象的内存地址是否相同( == )。
hashcode是根据对象的内存地址经哈希算法计算出来的值。
如果不重写hashCode()方法,当我们将某个自定义类对象作为key向map中放入时,会导致有参数完全相同的对象被存放到不到的bucket。