1 HashMap源码
本文关于HashMap大量参考了
https://blog.csdn.net/v123411739/article/details/78996181
https://joonwhee.blog.csdn.net/article/details/106324537
自己总结了红黑树相关知识点加上HashMap的面试知识点,意在HashMap这部分不被面试官问倒φ(* ̄0 ̄)。
1.1 前言
先了解以下几个点,有利于更好的理解 HashMap 的源码和阅读本文。
1、本文中头节点指的是 table 表上索引位置的节点,也就是链表的头节点。
2、根节点(root 节点)指的是红黑树最上面的那个节点,也就是没有父节点的节点。
3、红黑树的根节点不一定是索引位置的头节点(也就是链表的头节点),HashMap 通过 moveRootToFront 方法来维持红黑树的根结点就是索引位置的头结点,但是在 removeTreeNode 方法中,当 movable 为 false 时,不会调用 moveRootToFront 方法,此时红黑树的根节点不一定是索引位置的头节点,该场景发生在 HashIterator 的 remove 方法中。
4、转为红黑树节点后,链表的结构还存在,通过 next 属性维持,红黑树节点在进行操作时都会维护链表的结构,并不是转为红黑树节点,链表结构就不存在了。
5、在红黑树上,叶子节点也可能有 next 节点,因为红黑树的结构跟链表的结构是互不影响的,不会因为是叶子节点就说该节点已经没有 next 节点。
6、源码中一些变量定义:如果定义了一个节点 p,则 pl(p left)为 p 的左节点,pr(p right)为 p 的右节点,pp(p parent)为 p 的父节点,ph(p hash)为 p 的 hash 值,pk(p key)为 p 的 key 值,kc(key class)为 key 的类等等。源码中很喜欢在 if/for 等语句中进行赋值并判断,请注意。
7、链表中移除一个节点只需要将上一个节点的next指向下一个节点
8、红黑树在维护链表结构时,移除一个节点只需将上一个节点的next指向下一个节点,将下一个节点的pre指向上一个节点,其他操作同理。注:此处只是红黑树维护链表结构的操作,红黑树还需要单独进行红黑树的移除或者其他操作。
9、源码中进行红黑树的查找时,会反复用到以下两条规则:1)如果目标节点的 hash 值小于 p 节点的 hash 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。2)如果目标节点的 key 值小于 p 节点的 key 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。这两条规则是利用了红黑树的特性(左节点 < 根节点 < 右节点)。
10、源码中进行红黑树的查找时,会用 dir(direction)来表示向左还是向右查找,dir 存储的值是目标节点的 hash/key 与 p 节点的 hash/key 的比较结果。
定位哈希桶数组索引位置
// 代码1
static final int hash(Object key) { // 计算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代码2
int n = tab.length;
// 将(tab.length - 1) 与 hash值进行&运算
int index = (n - 1) & hash;
总结:
- 将key的hashcode向右移16位异或原本的hashcode
- 将重新计算的hashcode&(table.length-1)
常量
//初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//集合的最大容量是2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//缺省负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化阈值:链表的长度>8且集合的元素>=64,
static final int TREEIFY_THRESHOLD = 8;
//树降级为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//树化的另一个参数:当hash表中所有元素的个数超过64才会允许树化
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,称之为散列表
transient Node<K,V>[] table;
//存放元素的个数,HashMap中K-V的实时数量,不是table数组的长度
transient int size;
//增加和删除时该变量+1
transient int modCount;
//临界值,当实际大小(容量*负载因子)超过临界值时,会进行扩容,扩容之后的容量为之前容量的2倍
int threshold;
Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ... ...
}
TreeNode
//TreeNode继承了Node不仅仅是一个树节点还是一个双向链表
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
}
1.2 构造方法
table数组的创建是懒加载的,构造方法只是确定初始化容量。下面的代码摘自resize(),可以看到使用HashMap()那么创建table数组会使用默认的初始化容量16;而使用HashMap(int initialCapacity)和HashMap(int initialCapacity, float loadFactor)那么创建table数组会使用用户给定的初始化容量(初始化容量为2^n,解释见面试题19
)
else if (oldThr > 0) // oldThr被threshold赋值
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将默认的加载因子0.75赋值给loadFactor,并没有创建数组
}
// 指定“容量大小”,负载因子为0.75的HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadFactor;
//tableSizeFor返回一个大于等于当前initialCapacity的值,并且这个值一定是2的幂次方,为什么是2的幂次方看面试题19
this.threshold = tableSizeFor(initialCapacity);
}
1.3 put 方法
概述(面试直接背)
- 如果table数组没有初始化,那么先初始化之后计算索引位置将元素插入
- 如果table数组已经初始化计算索引位置,分为三种情况
- 如果该索引位置没有元素,新建节点放到该索引位置;如果该索引位置的元素和插入节点的元素key值相等考虑替换
- 如果该索引位置是链表的头结点,遍历链表,如果链表上有元素和插入节点的元素的key值相等考虑替换,否则新建节点放在链表尾部,此时考虑是否需要将链表转为红黑树
- 如果该索引位置是红黑树节点,从红黑树根节点进行遍历,如果红黑树上有元素和插入节点的key相等考虑替换,否则新建节点放到红黑树的相应位置,此时考虑红黑树的自平衡
- 如果插入节点的话,判断size>threshold考虑是否需要扩容
put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal
//onlyIfAbsent true表示如果key存在则不改变,false表示如果key存在则覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab表示引用当前hashmap的散列表
//p表示当前散列表的元素
//n表示当前散列表的长度
//i表示路由寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, i;
//开始时hashmap没有创建
if ((tab = table) == null || (n = tab.length) == 0)
//延迟初始化逻辑,第一次调用putval时会初始化HashMap对象中最耗费内存的散列表
n = (tab = resize()).length;
//寻址找到的桶位为空,直接将当前元素封装为node放入数组中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//e:e!=null 找到一个与当前要插入的key-value一致的key
//k:key临时元素
Node<K,V> e; K k;
//表示桶位中的该元素与当前插入的元素的key完全一致,后面需要进行替换操作
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);
//此时桶位所在结构是链表,并且链表的头元素和我们要插入的key不一致
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
//将元素插入到链表末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表的长度=TREEIFY_THRESHOLD树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//在链表中找到一个要插入的key-value一致的key,后面需要进行替换操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//onlyIfAbsent为fasle key-value中key相同,将value进行替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//modCount:表示散列表结构被修改(增加或者删除)的次数,替换node元素的value不计数
++modCount;
//插入新元素size自增,hashMap里面所有元素的size如果大于扩容阈值需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putTreeVal
从根节点开始遍历分为三种情况:
- 如果插入节点的hash值比当前节点的hash值小那么向左遍历
- 如果插入节点的hash值比当前节点的hash值大那么向右遍历
- 如果插入节点的hash值和当前节点的hash值相等
- 比较key值分为三种情况
- 如果相等考虑直接返回后面考虑是否要覆盖;
- 如果插入节点的key比当前节点的key小那么向左遍历
- 如果插入节点的key比当前节点的key大那么向右遍历
- 比较key值分为三种情况
- 如果向左遍历且左儿子为空那么在左节点插入新的节点,如果向右遍历且右儿子为空那么在右节点插入新的节点,如果上述条件不满足则继续遍历
- 最后红黑树自平衡,并且将根节点设置为链表的头结点
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
//kc表示key对应的class文件
Class<?> kc = null;
//初始化一个布尔变量searched为false,用于记录是否已经在搜索过程中搜索过节点。
boolean searched = false;
//获取到根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//使用一个无限循环遍历树的节点
for (TreeNode<K,V> p = root;;) {
//ph表示当前节点的hash值
//dir=-1表示向左边查找,dir=1表示向右边查找
int dir, ph; K pk;
//如果传入的hash值小于p节点的hash值,将dir赋值为-1,代表向p的左边查找树
if ((ph = p.hash) > h)
dir = -1;
//如果传入的hash值大于p节点的hash值,将dir赋值为1,代表向p的右边查找树
else if (ph < h)
dir = 1;
// 如果传入的hash值和key值等于p节点的hash值和key值, 则p节点即为目标节点, 返回p节点
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//此时hash值相等,但是发生了hash冲突导致key不相等或者不能被比较
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//如果之前没有查找过树节点接着判断
if (!searched) {
//q为找到的key-value中key相等的元素
TreeNode<K,V> q, ch;
//表示已经查询过该树节点和子节点,下次不会再次寻找
searched = true;
//遍历左子树和右子树,找到key-value中key相等的元素返回
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//如果没有key-value中key相等的元素,说明需要新增树节点,不过应该在哪里新增呢?
//tieBreakOrder 方法会根据键的类别(是数字还是字符串)和值的大小来判断两个键的顺序。如果目标键比当前节点的键小,则将 dir 赋值为 -1,否则赋值为 1,如果=0还是没有比较出来
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
//如果dir<0,并且树节点的左儿子为空说明可以将新值插入
//如果dir>0,并且树节点的左儿子为空说明可以将新值插入
//如果树节点的左儿子或者右儿子不为空,需要进行下一轮循环
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
// 创建一个新的树节点x
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//将树节点插入合适位置
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//将树节点的.next指向新节点
xp.next = x;
//将新节点的父节点和前一个节点设置为当前树节点
x.parent = x.prev = xp;
// 如果xpn不为空,则将xpn的prev节点设置为x节点,与上文的x节点的next节点对应
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 进行红黑树的插入平衡调整,moveRootToFront 方法用于将红黑树的根节点置于链表的顶部
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
tieBreakOrder
// 用于不可比较或者hashCode相同时进行比较的方法, 只是一个一致的插入规则,用来维护重定位的等价性。
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
moveRootToFront
/**
* 将root放到头节点的位置
* 如果当前索引位置的头节点不 是root节点, 则将root的上一个节点和下一个节点进行关联,
* 将root放到头节点的位置, 原头节点放在root的next节点上
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
// 1.校验root是否为空、table是否为空、table的length是否大于0
if (root != null && tab != null && (n = tab.length) > 0) {
// 2.计算root节点的索引位置
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 3.如果该索引位置的头节点不是root节点,则该索引位置的头节点替换为root节点
if (root != first) {
Node<K,V> rn;
// 3.1 将该索引位置的头节点赋值为root节点
tab[index] = root;
TreeNode<K,V> rp = root.prev; // root节点的上一个节点
// 3.2 和 3.3 两个操作是移除root节点的过程
// 3.2 如果root节点的next节点不为空,则将root节点的next节点的prev属性设置为root节点的prev节点
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
// 3.3 如果root节点的prev节点不为空,则将root节点的prev节点的next属性设置为root节点的next节点
if (rp != null)
rp.next = rn;
// 3.4 和 3.5 两个操作将first节点接到root节点后面
// 3.4 如果原头节点不为空, 则将原头节点的prev属性设置为root节点
if (first != null)
first.prev = root;
// 3.5 将root节点的next属性设置为原头节点
root.next = first;
// 3.6 root此时已经被放到该位置的头节点位置,因此将prev属性设为空
root.prev = null;
}
// 4.将传入的节点作为根节点,遍历所有节点,校验节点的合法性,主要是保证该树符合红黑树的规则
assert checkInvariants(root);
}
}
treeifyBin
将链表树化的方法分成两部:
- 判断链表长度大于8并且size>=64满足才树化,否则扩容
- 从链表头结点开始遍历:1、将链表节点转为红黑树节点 2、构建双向链表
- 通过treeify方法从双向链表头节点开始遍历,将双向链表转成红黑树
//将链表节点转为红黑树节点
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//链表长度等于8但是数组的长度还没有超过64,链表不会转为红黑树而是扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//根据hash值计算索引值,将该索引值赋值给e,从e开始遍历该索引位置的链表
else if ((e = tab[index = (n - 1) & hash]) != null) {
//定义红黑树的头节点和尾结点
TreeNode<K,V> hd = null, tl = null;
do {
//将链表节点转为红黑树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
//如果是第一次遍历,赋值头结点
if (tl == null)
hd = p;
//如果不是第一次遍历则将当前节点的pre设置为上一个节点
//当前节点的next设置为下一个节点
else {
p.prev = tl;
tl.next = p;
}
//将最后一个节点设置为尾结点
tl = p;
} while ((e = e.next) != null);//遍历链表
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
treeify
/**
* 双向链表树化的方法
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 1.将调用此方法的节点赋值给x,以x作为起点,开始进行遍历
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next; // next赋值为x的下个节点
x.left = x.right = null; // 将x的左右节点设置为空
// 2.如果还没有根节点, 则将x设置为根节点
if (root == null) {
x.parent = null; // 根节点没有父节点
x.red = false; // 根节点必须为黑色
root = x; // 将x设置为根节点
}
else {
K k = x.key; // k赋值为x的key
int h = x.hash; // h赋值为x的hash值
Class<?> kc = null;
// 3.如果当前节点x不是根节点, 则从根节点开始查找属于该节点的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 4.如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找
if ((ph = p.hash) > h)
dir = -1;
// 5.如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找
else if (ph < h)
dir = 1;
// 6.走到这代表x的hash值和p的hash值相等,则比较key值
else if ((kc == null && // 6.1 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 6.2 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值
// 7.dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 8.x和xp节点的属性设置
x.parent = xp; // x的父节点即为最后一次遍历的p节点
if (dir <= 0) // 如果时dir <= 0, 则代表x节点为父节点的左节点
xp.left = x;
else // 如果时dir > 0, 则代表x节点为父节点的右节点
xp.right = x;
// 9.进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
// 10.如果root节点不在table索引位置的头节点, 则将其调整为头节点
moveRootToFront(tab, root);
}
1.4 resize 扩容方法
概述(面试直接背诵)
//为了解决hash冲突导致链化严重,影响查询效率扩容会缓解该问题
final Node<K,V>[] resize() {
//oldTable引用扩容之前的hash表
Node<K,V>[] oldTab = table;
//表示扩容之前的table[]的长度,table还没有初始化时为0,否则就是之前hash表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//表示扩容之前的扩容阈值,即触发本次扩容的阈值
int oldThr = threshold;
//newCap:扩容之后table数组的大小
//newThr:扩容之后下次触发扩容的条件·
int newCap, newThr = 0;
//hashMap中的散列表已经初始化过了,是正常的扩容·············
if (oldCap > 0) {
//扩容之前table数组大小已经达到最大阈值不扩容,且设置扩容条件为int最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//oldCap左移一位赋值给newCap作为新的数组的长度,oldThr左移一位作为新newThr的长度(1)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//oldCap = 0,说明hashmap中的散列表是null
//1、new HashMap(initCap,loadFactor);
//2、new HashMap(initCap)
//3、new HashMap(map),并且map有数据
//上面这三种方法会给初始化threshold(threshold = tableSizeFor(initialCapacity) ),这个值作为初始化数组的长度,并且0.75*初始化数组的长度作为下次扩容的阈值
//4、new HashMap()方法不会初始化threshold所以将16作为初始化数组的长度,并且12作为数组下次扩容的阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//oldCap = 0 ,oldThr = 0
else { // zero initial threshold signifies using defaults
//newCap =16,newThr =16*0.7
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//newThr = newCap*loadFactor
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;
//说明此次扩容之前,table不为null,从头开始遍历
//如果table==null那么直接跳出resize()方法在put方法中添加第一个元素返回
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//当前node节点
Node<K,V> e;
//说明当前桶位有数据,但是数据是单个数据还是链表或者红黑树并不清楚
if ((e = oldTab[j]) != null) {
//方便:GC回收内存
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
//低位链表:存放在扩容之后的数组的下标位置与当前数组的下标一致
//高位链表:存放在扩容之后的数组的下标位置为当前数组下标+扩容之前数组的长度
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//遍历当前桶位的链表
do {
next = e.next;
//判断元素应该在扩容的HashMap的低位链表还是高位链表
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);
//遍历结束
//将高位/低位链表的链表头连到newTable中,将高位/低位链表的链表尾置为空
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/**
* 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this; // 拿到调用此方法的节点
TreeNode<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点
int lc = 0, hc = 0;
// 1.以调用此方法的节点开始,遍历整个红黑树节点
for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历
next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点
e.next = null; // 同时将老表的节点设置为空,以便垃圾收集器回收
// 2.如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null) // 如果loTail为空, 代表该节点为第一个节点
loHead = e; // 则将loHead赋值为第一个节点
else
loTail.next = e; // 否则将节点添加在loTail后面
loTail = e; // 并将loTail赋值为新增的节点
++lc; // 统计原索引位置的节点个数
}
// 3.如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
else {
if ((e.prev = hiTail) == null) // 如果hiHead为空, 代表该节点为第一个节点
hiHead = e; // 则将hiHead赋值为第一个节点
else
hiTail.next = e; // 否则将节点添加在hiTail后面
hiTail = e; // 并将hiTail赋值为新增的节点
++hc; // 统计索引位置为原索引+oldCap的节点个数
}
}
// 4.如果原索引位置的节点不为空
if (loHead != null) { // 原索引位置的节点不为空
// 4.1 如果节点个数<=6个则将红黑树转为链表结构
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
// 4.2 将原索引位置的节点设置为对应的头节点
tab[index] = loHead;
// 4.3 如果hiHead不为空,则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (hiHead != null)
// 4.4 以loHead为根节点, 构建新的红黑树
loHead.treeify(tab);
}
}
// 5.如果索引位置为原索引+oldCap的节点不为空
if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空
// 5.1 如果节点个数<=6个则将红黑树转为链表结构
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
// 5.2 将索引位置为原索引+oldCap的节点设置为对应的头节点
tab[index + bit] = hiHead;
// 5.3 loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
// 已经被改变, 需要重新构建新的红黑树
if (loHead != null)
// 5.4 以hiHead为根节点, 构建新的红黑树
hiHead.treeify(tab);
}
}
}
untreeify方法
/**
* 将红黑树节点转为链表节点, 当节点<=6个时会被触发
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null; // hd指向头节点, tl指向尾节点
// 1.从调用该方法的节点, 即链表的头节点开始遍历, 将所有节点全转为链表节点
for (Node<K,V> q = this; q != null; q = q.next) {
// 2.调用replacementNode方法构建链表节点
Node<K,V> p = map.replacementNode(q, null);
// 3.如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点
if (tl == null)
hd = p;
// 4.否则, 将尾节点的next属性设置为当前节点p
else
tl.next = p;
tl = p; // 5.每次都将tl节点指向当前节点, 即尾节点
}
// 6.返回转换后的链表的头节点
return hd;
}
1.5 get方法
public V get(Object key) {
Node<K,V> e;
//数据put时就hash了一次
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
//tab:引用当前hasmap的散列表
//first:桶位中的头元素
//e:临时node元素
//n:table数组长度
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//当前数组不为空并且当前桶位first不为空,当前first可能为单独元素或者是链表头结点或者是树的头结点
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;
//链表or树的头结点
if ((e = first.next) != null) {
//树的头结点
if (first instanceof TreeNode)
//遍历树找到对应的key将key-value返回
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历链表,找到对应的key将key-value返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
getTreeNode
final TreeNode<K,V> getTreeNode(int h, Object k) {
//找到当前节点的根节点,从根节点开始遍历
return ((parent != null) ? root() : this).find(h, k, null);
}
find
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
//从当前位置开始查找
TreeNode<K,V> p = this;
//定义一个do while循环,遍历到叶子节点时循环终止
do {
//ph表示当前节点的hash
//pl表示当前节点的左儿子节点,pr表示当前节点的有儿子节点
//pk表示当前节点的key值
//q表示在当前右子树中找到的节点
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
//如果当前节点的hash值比给定hash值大,则继续在左子树中查找,将左子节点赋值给p
if ((ph = p.hash) > h)
p = pl;
//如果当前节点的hash值比给定hash值小,则继续在右子树中查找,将右子节点赋值给p。
else if (ph < h)
p = pr;
//如果当前节点的hash值等于给定hash值,且当前节点的key值等于给定的key值,那么就找到了要查找的节点,直接返回p。(因为存在hash冲突及时hash值相等但是key也可能不等)
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//当当前节点的 hash 值等于目标节点的 hash 值时,需要进一步比较当前节点的 key 值和目标节点的 key 值是否相等,如果相等则返回当前节点。如果不相等,则需要继续查找。这时有两种情况:
//如果当前节点的左子树为空,则说明目标节点不存在于左子树中,因此将当前节点 p 指向它的右子节点 pr,继续查找
else if (pl == null)
p = pr;
//如果当前节点的右子树为空,则说明目标节点不存在于右子树中,因此将当前节点 p 指向它的左子节点 pl,继续查找
else if (pr == null)
p = pl;
//kc代表的是key的类对象,k代表要查找的key,pk代表当前节点的key,dir代表在比较key的值时的比较结果
else if ((kc != null ||
//这个函数的功能是判断对象x是否实现了Comparable接口,并返回x的Class对象。如果x实现了Comparable接口,则返回x的Class对象,否则返回null
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
//当前节点的 hash 值等于目标节点的 hash 值,但是不能比较两个key的大小,所以最差的情况先在右子树上进行遍历
else if ((q = pr.find(h, k, kc)) != null)
return q;
//没有遍历到在从左子树遍历
else
p = pl;
} while (p != null);
return null;
}
1.6 remove方法
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//matchValue为true表示当key和value全部匹配才删除
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//tab引用当前hashmap中的散列表
//p当前node元素
//n表示当前散列表数组长度
//index
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node表示查找到的结果
//e表示当前node的下一个元素
Node<K,V> node = null, e; K k; V v;
//第一种情况:删除节点在first上
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//第二种情况:删除节点在红黑树上
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//第三种情况:删除节点在链表上
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//node如果不为空的话,按照key删除要查找的数据
//matchValue如果是true除了比较key一致还要比较value
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//第一种情况:node是树节点,说明需要进行树节点移除操作
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//第二种情况:桶位头元素即为查找结果,则将该元素的的下一个元素放到桶位
else if (node == p)
tab[index] = node.next;
//第三种情况:链表中的节点为删除节点
else
p.next = node.next;
//modCount为增加或者删除的次数
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
removeTreeNode
/**
* 目的就是移除调用此方法的节点,也就是该方法中的 this 节点。移除包括链表的处理和红黑树的处理
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// --- 链表的处理start ---
int n;
// 1.table为空或者length为0直接返回
if (tab == null || (n = tab.length) == 0)
return;
// 2.根据hash计算出索引的位置
int index = (n - 1) & hash;
// 3.将索引位置的头节点赋值给first和root
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 4.该方法被将要被移除的node(TreeNode)调用, 因此此方法的this为要被移除node节点,
// 将node的next节点赋值给succ节点,prev节点赋值给pred节点
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 5.如果pred节点为空,则代表要被移除的node节点为头节点,
// 则将table索引位置的值和first节点的值赋值为succ节点(node的next节点)即可
if (pred == null)
tab[index] = first = succ;
else
// 6.否则将pred节点的next属性设置为succ节点(node的next节点)
pred.next = succ;
// 7.如果succ节点不为空,则将succ的prev节点设置为pred, 与前面对应
if (succ != null)
succ.prev = pred;
// 8.如果进行到此first节点为空,则代表该索引位置已经没有节点则直接返回
if (first == null)
return;
// 9.如果root的父节点不为空, 则将root赋值为根节点
if (root.parent != null)
root = root.root();
// 10.通过root节点来判断此红黑树是否太小, 如果是则调用untreeify方法转为链表节点并返回
// (转链表后就无需再进行下面的红黑树处理)
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// --- 链表的处理end ---
// --- 以下代码为红黑树的处理 ---
// 11.将p赋值为要被移除的node节点,pl赋值为p的左节点,pr赋值为p 的右节点
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// 12.如果p的左节点和右节点都不为空时
if (pl != null && pr != null) {
// 12.1 将s节点赋值为p的右节点
TreeNode<K,V> s = pr, sl;
// 12.2 向左一直查找,跳出循环时,s为没有左节点的节点
while ((sl = s.left) != null)
s = sl;
// 12.3 交换p节点和s节点的颜色
boolean c = s.red; s.red = p.red; p.red = c;
TreeNode<K,V> sr = s.right; // s的右节点
TreeNode<K,V> pp = p.parent; // p的父节点
// --- 第一次调整和第二次调整:将p节点和s节点进行了位置调换 ---
// 12.4 第一次调整
// 如果p节点的右节点即为s节点,则将p的父节点赋值为s,将s的右节点赋值为p
if (s == pr) {
p.parent = s;
s.right = p;
}
else {
// 将sp赋值为s的父节点
TreeNode<K,V> sp = s.parent;
// 将p的父节点赋值为sp
if ((p.parent = sp) != null) {
// 如果s节点为sp的左节点,则将sp的左节点赋值为p节点
if (s == sp.left)
sp.left = p;
// 否则s节点为sp的右节点,则将sp的右节点赋值为p节点
else
sp.right = p;
}
// s的右节点赋值为p节点的右节点
if ((s.right = pr) != null)
// 如果pr不为空,则将pr的父节点赋值为s
pr.parent = s;
}
// 12.5 第二次调整
// 将p的左节点赋值为空,pl已经保存了该节点
p.left = null;
// 将p节点的右节点赋值为sr,如果sr不为空,则将sr的父节点赋值为p节点
if ((p.right = sr) != null)
sr.parent = p;
// 将s节点的左节点赋值为pl,如果pl不为空,则将pl的父节点赋值为s节点
if ((s.left = pl) != null)
pl.parent = s;
// 将s的父节点赋值为p的父节点pp
// 如果pp为空,则p节点为root节点, 交换后s成为新的root节点
if ((s.parent = pp) == null)
root = s;
// 如果p不为root节点, 并且p是pp的左节点,则将pp的左节点赋值为s节点
else if (p == pp.left)
pp.left = s;
// 如果p不为root节点, 并且p是pp的右节点,则将pp的右节点赋值为s节点
else
pp.right = s;
// 12.6 寻找replacement节点,用来替换掉p节点
// 12.6.1 如果sr不为空,则replacement节点为sr,因为s没有左节点,所以使用s的右节点来替换p的位置
if (sr != null)
replacement = sr;
// 12.6.1 如果sr为空,则s为叶子节点,replacement为p本身,只需要将p节点直接去除即可
else
replacement = p;
}
// 13.承接12点的判断,如果p的左节点不为空,右节点为空,replacement节点为p的左节点
else if (pl != null)
replacement = pl;
// 14.如果p的右节点不为空,左节点为空,replacement节点为p的右节点
else if (pr != null)
replacement = pr;
// 15.如果p的左右节点都为空, 即p为叶子节点, replacement节点为p节点本身
else
replacement = p;
// 16.第三次调整:使用replacement节点替换掉p节点的位置,将p节点移除
if (replacement != p) { // 如果p节点不是叶子节点
// 16.1 将p节点的父节点赋值给replacement节点的父节点, 同时赋值给pp节点
TreeNode<K,V> pp = replacement.parent = p.parent;
// 16.2 如果p没有父节点, 即p为root节点,则将root节点赋值为replacement节点即可
if (pp == null)
root = replacement;
// 16.3 如果p不是root节点, 并且p为pp的左节点,则将pp的左节点赋值为替换节点replacement
else if (p == pp.left)
pp.left = replacement;
// 16.4 如果p不是root节点, 并且p为pp的右节点,则将pp的右节点赋值为替换节点replacement
else
pp.right = replacement;
// 16.5 p节点的位置已经被完整的替换为replacement, 将p节点清空, 以便垃圾收集器回收
p.left = p.right = p.parent = null;
}
// 17.如果p节点不为红色则进行红黑树删除平衡调整
// (如果删除的节点是红色则不会破坏红黑树的平衡无需调整)
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 18.如果p节点为叶子节点, 则简单的将p节点去除即可
if (replacement == p) {
TreeNode<K,V> pp = p.parent;
// 18.1 将p的parent属性设置为空
p.parent = null;
if (pp != null) {
// 18.2 如果p节点为父节点的左节点,则将父节点的左节点赋值为空
if (p == pp.left)
pp.left = null;
// 18.3 如果p节点为父节点的右节点, 则将父节点的右节点赋值为空
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
// 19.将root节点移到索引位置的头节点
moveRootToFront(tab, r);
}
2 jdk1.7和jdk1.8
- 7 = 数组 + 链表,8 = 数组 + 链表 + 红黑树
- 7 中是头插法,多线程容易造成环,8 中是尾插法
- 7 的扩容是全部数据重新定位,8 中是位置不变或者当前位置 + 旧 size 大小来实现
- 7 是先判断是否要扩容再插入,8 中是先插入再看是否要扩容
红黑树
二叉树
树的节点最多只能有两个子节点的一种形式称为二叉树
二叉搜索树
在二叉树的基础上加额外条件
若左子树不为空,那么左子树上的所有节点的值小于它的根节点的值
若右子树不为空,那么右子树上的所有节点的值大于它的根节点的值
二叉搜索树的遍历顺序:一般二叉树的遍历方式有前-中-后序遍历三种,但是一般二叉搜索树的遍历按照中序遍历,因为这样遍历是有顺序的。
二叉搜索树查找最小值和最大值:要找最小值先找到根节点,然后一直找这个节点的左节点,直到没有左节点的节点,那么这个节点就是最小值;同理要找最大值,一直找根节点的右节点直到找到没有右节点的节点为止,那么这个节点就是最大值。
二叉搜索树删除节点:删除节点有三种情况1、该节点是叶子节点,只需要将该节点的父节点的引用指向null即可,2、该节点有一个子节点,只需要将父节点原本指向该节点的引用指向该节点的子节点即可3、该节点有两个子节点,二叉搜索树中序遍历的后继节点替换删除节点
如果想要删除15节点可以将20节点替换掉15节点。
删除有必要吗?删除会改变树的结构,我们只需要在Node类中增加一个表示字段isDelete,该字段为true则表示该节点已经被删除,反之则没有删除。
二叉搜索树的时间复杂度分析
1、二分查找算法时间复杂度:数据源必须是有序数组o(logn)
2、二分查找算法的最大缺陷是什么:强制依赖有序数组性能才会得到不错的性能。
3、数组有什么缺陷?一是插入删除时间复杂度比较高;二是扩容时间复杂度比较高
4、怎么样才能拥有二分查找的高性能又有链表一样的灵活性呢?二叉搜索树
5、普通的二叉搜索树有什么缺陷?普通的二叉树可能退化链表,此时查找效率为o(n)
AVL树(二叉平衡树)
具有二叉搜索树的全部特性,每个节点的左子树和右子树的高度差最多等于1。AVL树可以保证不会使得大量的节点偏向一边。
红黑树
如果在那种插入和删除比较频繁的场景中,平衡树需要频繁进行调整,会使得平衡树的性能大打折扣,为了解决这个问题于是有了红黑树!
红黑树的性质(重点)
- 每个节点不是红色就是黑色
- 不可能有连在一起的红色节点
- 根节点都是黑色
- 每个红色节点的两个子节点都是黑色
红黑树并不是一颗完美平衡的二叉树,但是左子树和右子树的黑色节点层数是相等的(称之为黑色完美平衡)
红黑树能够自平衡依靠:左旋、右旋、变色
变色:节点的颜色由红色变成黑色或者由黑色变成红色
左旋:以某个节点作为支点(旋转节点),其右子节点变为旋转节点的父节点,右子节点的左子节点变成旋转节点的右子节点,左子节点保持不变。
右旋:以某个节点作为支点(旋转节点),其左子节点变成旋转节点的父节点,左子节点的右子节点变成旋转节点的左子节点,右子节点保持不变。
红黑树的查找和普通的二叉搜索树的查找一致
红黑树插入:1查找插入的位置;2插入后自平衡
注意:插入节点必须是红色节点,红色节点在父节点为黑色节点时,红黑树的黑色平衡没有被破坏不需要做自平衡,但是如果插入的节点是黑色,那么自平衡一定被破坏
约定
红黑树旋转和变色规则
1、变颜色的情况:当前节点的父亲是红色,且叔叔节点也是红色
(1)把父节点设为黑色
(2)把叔叔节点也设为黑色
(3)把祖父设为红色
(4)把指针定义到祖父节点设为当前要操作的
2、左旋:当前父节点是红色,叔叔是黑色的时候,且当前的节点是右子树,左旋以父节点作为左旋
3、右旋:当前父节点是红色,叔叔节点是黑色的时候,且当前的节点是左子树,右旋
(1)把父节点变为黑色
(2)把祖父节点变为红色(爷爷)
(3)把祖父节点旋转(爷爷)
节点的层次
从根开始定义,根为第一层,根的子节点为第二层,以此类推
深度
对于任意节点n,n的深度为根到n的唯一路径长,根的深度为0
高度
任意节点n到树叶的最长路径长,所有树叶的高度为0
HashMap并发死链
多线程并发扩容的情况下,链表可能形成死链(环形链表)。一旦有任何查找元素的动作,线程将会陷入死循环,从而引发 CPU 使用率飙升。死链发生在jdk1.8之前,因为元素的排列使用了头插法即后插入的元素排列靠前。
迁移的源代码
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;//假设两个线程同时执行到这里,那么e都指的是3,next都指的是2
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
单线程
第一次for循环结束
链表全部遍历结束
多线程
多线程下可能同时对上面的初始map进行扩容
此时线程一扩容完毕,线程二进行扩容,但是线程一已经将2,3的顺序调换,可想而知后面会出现死链
线程二第一次for循环
线程二第二次for循环
线程二第三次for循环出现了链表循环
究其原因是第一个线程扩容完毕,会将链表的顺序发生置换,但是线程二依然使用transfer方法的逻辑,所以发生了链表循环的问题。JDK1.8使用尾插法,但是HashMap在JDK1.8仍然不适合用于并发场景,依然是无法避免并发扩容情况下的死链问题(红黑树也可能也会死循环)。(并发赋值时被覆盖、size 计算问题)
HashMap面试题
1.问:说说你对hash算法的理解
回答:将任意长度转为固定长度的算法
追问:hash算法任意长度的输入 转化为了 固定长度的输出,会不会有问题呢?
回答: hash冲突
追问:hash冲突能避免么?
回答:比如一共有9个苹果放到10个抽屉,这样一定有两个苹果放入一个抽屉,所以hash冲突没有办法避免
2问.你认为好的hash算法,应该考虑点有哪些呢?
回答:(1)正向快速:给定关键字和散列函数,有限时间和资源内能计算出散列(hash)值(2)逆向困难:只给定hash值,你很难逆向算出关键字(3)输入敏感:原始输入信息修改一点点,产生的hash值也能有较大的不同(4)冲突避免:你很难找到两个不同的关键字算出的散列值是相同的(发生冲突)
追问:发生Hash冲突怎么办?(HashMaP如何减少hash碰撞)
回答:1、链地址法,如果发生hash冲突,将冲突的元素连接成链表。2、再hash法,当hash地址发生冲突使用其他的函数计算另一个hash函数地址。3、将hash表分为基本表和溢出表,发生冲突的元素放在溢出表中。
3问.HashMap中存储的结构是什么样的呢?
回答:jdk1.8 HashMap的结构是数组+链表+红黑树,里面元素都是node结构,node里面包括key,value,next和hash,next字段用于hash冲突时连接链表。链表会导致查询速度比较慢,此时链表形成红黑树。
4问.创建HashMap时,不指定散列表数组长度,初始长度是多少呢?
回答:默认16
追问:散列表是new HashMap() 时创建的么?
回答:散列表是懒加载机制第一次put数据的时候才创建
6问.链表转化为红黑树,需要达到什么条件呢?
回答:散列表数组长度大于等于64+链表长度超过8达到9个,如果数组table的长度没有到达64那么仅仅会发生resize操作。
追问:什么时候红黑树转为链表?
回答:当同一个索引位置的节点移除之后达到6个,并且该索引位置的节点为红黑树节点,会触发红黑树转为链表
7问.Node对象内部的hash字段,这个hash值是key对象的hashcode()返回值么?
回答:不是
追问:这个hash值是怎么得到呢?
回答:key的hashcode经过加工得到,key的hashcode向右移动16位异或hashcode得到
追问:hash字段为什么采用高低位异或?
回答:因为hash寻址算法是hash&(table.length-1),如果table.length比较小的时候如果采用之前的hashcode那么高16位相当于浪费了
追问:为什么HashMap的数组长度使用2的幂?
回答:因为这样hash对于数组进行取模运算就转换成了位运算,这样增加了计算速度,同时可以减少hash冲突
8问.HashMap put 写数据的具体流程,尽可能的详细点!
回答:第一次put时会创建table数组,key的hashcode经过右移与hashcode异或运算之后&table.length-1得到桶位,根据桶位的状况不同,情况也不同。
第一种是放的值是桶位的第一个元素
第二种情况是放的值是在链表上
第三种情况是放的值在树上。
如果当前位置已经有值根据onlyIfAbsent选择是否进行替换,如果HashMap中没有该key在对应位置增加该元素,需要注意的是如果在链表增加元素的时候增加之后需要判断是否满足树化条件。最后需要判断HashMap里面的元素总数是否超过扩容阈值,如果超过需要进行扩容。
9问.红黑树的写入操作,是怎么找到父节点的,找父节点流程?
回答:treeNode在node的基础上增加字段分别四指向父节点的parent,指向左子节点的left和指向右子节点的right,表示颜色的red
TreeNode数据结构,简单说下。
11问.红黑树的原则有哪些呢?
回答
12问.JDK8 hashmap为什么引入红黑树?解决什么问题?
回答:解决hash冲突,本来散列表table数组的查询效率为o(1),但是链化严重时查询效率退化为o(n),所以才引入的红黑树,红黑树是一个接近于自平衡的二叉排序树,时间复杂度为o(logn)
提问:为什么hash冲突后性能变低了?【送分题】链表查询只能通过next指针一个一个进行遍历。
13.hashmap 什么情况下会触发扩容呢?
put操作如果没有创建table[]数组会触发扩容,如果HashMap元素超过扩容阈值触发扩容,链表的长度超过8但是数组长度没有超过64不会树化而是扩容。
追问:触发扩容后,会扩容多大呢?算法是什么?
回答:table数组的长度必须是2的n次幂,扩容每一次都是上一次table.length左移一位运算
追问:为什么采用位移运算,不是直接*2?
回答:cpu不支持乘法运算,乘法运算都是转换为加法运算实现的,相反位运算对于cpu来说效率比较高
14问.hashmap扩容后,老表的数据怎么迁移到扩容后的表的呢?
15.hashmap扩容后,迁移数据发现该slot是颗红黑树,怎么处理呢?
16 HashMap可以实现同步吗?
Map m = Collections.synchronizedMap(hashmap)
追问:HashMap和Hashtable的区别
回答:HashMap可以等价于Hashtable除了HashMap是非synchronized,并且可以接收null的key和value,HashMap的迭代器使用的是Iterator但是Hashtable使用的迭代器是enumerator。在迭代的过程中如果其他线程改变了HashMap的结构将会抛出ConcurrentModificationException异常,但是迭代器本身的remove方法不会抛出异常
追问:那么快速失败机制底层是怎么实现的呢?
回答:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历,。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)
追问:那么安全失败机制底层是怎么实现的?
回答:用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历,由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException。基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
17 问.为什么要重写hashmap中的equals()和hashcode()方法?
回答:https://blog.csdn.net/Sunsetsunset/article/details/122463115
18 问.HashMap什么时候进行扩容,它是怎么样进行扩容的?
HashMap扩容主要取决于两个元素:一个是Capacity即当前HashMap的数组长度,一个是LoadFactor负载因子默认为0.75,当Map中的元素(包括数组,链表,红黑树)超过了16*0.75之后进行扩容。
19 问. 为什么散列表的长度始终是2^n呢?
回答:1 使用位运算代替取余运算,定位哈希桶数组索引位置的位置时使用hash & (length-1),而hash % length == hash & (length-1)` 的前提是 length 是 2 的 n 次幂;2 保证散列的均匀性2的n次幂-1是n个1可以保证散列的均匀性比如:例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
20 问. 为什么负载因子使用0.75而不是其他的值?
这个也是在时间和空间上权衡的结果。如果值较高,例如1,此时会减少空间开销,但是 hash 冲突的概率会增大,增加查找成本;而如果值较低,例如 0.5 ,此时 hash 冲突会降低,但是有一半的空间会被浪费,所以折衷考虑 0.75 似乎是一个合理的值
21 为什么使用8作为树化阈值?
红黑树节点大小约为链表节点的2倍,在节点太小时红黑树的查找性能优势并不明显,付出两倍空间作者认为并不值得。
理想情况下使用随机的哈希码,节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的公式计算,链表中节点个数为8时的概率为 0.00000006,这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个较合理的数字
22 为什么转回链表节点使用6而不是复用8?
如果我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗
23 如何定位哈希桶数组索引位置?
首先将key的hashcode向右移16位异或原本的hashcode然后重新计算的hashcode&(table.length-1)
追问:为什么要将key的hashcode向右移16位异或原本的hashcode?
目的让key的低16位有高16位的特征,开始时table非常小可能是16,32,64的时候路由寻址(table.length-1)&key的hashcode,那么结果只能有hashcode的低位的特征。
24 threshold除了存放扩容阈值之外还有其他作用吗?
新建HashMap时,threshold还会被用来存放初始化容量,HashMap知道我们第一次插入节点时才会将table初始化
25 HashMap的默认初始化容量时多少?HashMap的容量有什么限制吗?
默认初始化容量时16,如果使用构造方法给一个initialCapacity,最后的初始化容量会转为2^n
26 各种Map的选择
27 为什么红黑树和链表都是通过e.hash&oldCap==0来定位在新表的索引位置?
因为每次扩容都是*2,一个索引处的链表或者红黑树就会分成两堆。比如原本散列表的长度是16,那么之前就是hashcode&1111,即在老表中的索引值取决于后四位;新的散列表的长度是32,那么现在就是hashcode&11111,如果在之前在一个索引处那么可以知道后四位是相同的,所以最后的结果只取决于10000即可。10000也就是oldCap,这样如果是hashcode的第五位是0就在原索引位置,如果hashcode的第五位是1就在原索引位置+oldCap处正好可以分成两堆。