数据结构
上图有一个错误——链表转为红黑树:链表长度大于等于8时,并且数组长度大于64时,链表转为红黑树。
属性
int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始化默认容量==16
int MAXIMUM_CAPACITY = 1 << 30;//最大容量
float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子
int TREEIFY_THRESHOLD = 8;//转为红黑树阈值
int UNTREEIFY_THRESHOLD = 6;//转为链表阈值
int MIN_TREEIFY_CAPACITY = 64;//扩容——数组——最小阈值
transient Node<K,V>[] table;//数组存放的是节点或者链表或者红黑树
transient Set<Map.Entry<K,V>> entrySet;
transient int size;//元素数量
transient int modCount;//修改次数
int threshold;//扩容门槛
final float loadFactor;//加载因子
构造方法
分别为:无参、 带容量大小和加载因子的构造方法、仅带容量大小的构造方法、 带一个Map的构造方法
//其实构造方法没意思,就是设置一个加载因子和容量
//无参构造
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; //设置加载因子 = 0.75
}
//仅带容量大小的构造方法
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//带容量大小和加载因子的构造方法、
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
// 带一个Map的构造方法
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
添加K-V
主要分两部分:
1、根据key得到hash值,再根据hash值结合数组长度进行模运算,得到一个 int 值(A),这个 A值就是 Entry 要储存在数组的位置(下标)。
2、数组下标A没有Entry,没有值,使用 resize 方法初始化,直接生成新的节点在当前索引位置上
3、数组下标A有Entry,接下来就是我们去解决hash冲突。
4、记录 HashMap 的数据结构发生了变化
思路:
科普:其实 K-V 存储到HashMap数组中后会变成 Entry对象
HashMap<String, Integer> hashMap = new HashMap<String, Integer>();
hashMap.put("1",1);
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//计算key的hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//参数:hash:通过 hash 算法计算出来的值。onlyIfAbsent:默认 false,在默认情况下,添加数据,如果出现相同key,后添加的数据会覆盖之前的数据。evict:如果为false,则表处于创建模式。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//n 为数组长度、i为数组索引下标、p为i下标位置的 Node 值、tab==table
Node<K,V>[] tab; Node<K,V> p; int n, i;
//1、如果数组为空,使用resize()初始化数组(2的幂展开数组容量)
if ((tab = table) == null || (n = tab.length) == 0)//table存储 Entry 的数组
n = (tab = resize()).length;
//2、如果当前索引位置是空的,直接生成新的节点在当前索引位置上
if ((p = tab[i = (n - 1) & hash]) == null)//当前索引:key计算出来的数组坐标===(hash&(n-1))。注意:随着数组容量 n改变,key可以取的范围也在变大,但是不会超出 n的取值访问。
tab[i] = newNode(hash, key, value, null);//hash.key、value、next
//3、当前索引位置不为空,接下来就是我们去解决hash冲突
else {
/*
*解决hash冲突
* 如果key的hash和值都相等、直接把当前下标位置的Node值赋给临时遍历e。
* 如果p是红黑树:红黑树添加节点
* 如果p是链表
* 找的新节点位置后的操作:
*/
Node<K,V> e; K k;//e 当前节点(不是数组下标的Entry)的临时变量
//数组下标为i下标位置的 Node 值等于新节点。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//p是红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//p是链表,添加节点几种情况(无非就是修改或添加节点):frist.next = null、链表中有和新节点相同的节点、整个链表没有相同新节点
for (int binCount = 0; ; ++binCount) {//binCount 一个计数器
if ((e = p.next) == null) {//链表的第二给节点为空,说明链表中没有找到相同的Node,所以尾部添加新节点即可
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) //当链表长度大于等于8时,链表转为红黑树。 TREEIFY_THRESHOLD = 8
treeifyBin(tab, hash);
break;
}
//链表某给节点位置与新建节点相同,跳出遍历。修改e.value=value即可
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//新来节点与之前节点相同,后来节点值覆盖原来节点的值,即e.value=value
//头节点或链表中的节点有可能会发生Node相同的情况。
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;//因为有相同节点,后来节点值覆盖原来节点的值
afterNodeAccess(e);//用来回调移除最早放入Map的对象
return oldValue;
}
}
//4、记录 HashMap 的数据结构发生了变化
++modCount;
if (++size > threshold)//判断是否需要扩容——扩容条件:元素数量到达容量的四分之三,数组开始扩容,容量为2的幂次方
resize();
afterNodeInsertion(evict);//HashMap中没有对这个方法做操作。
return null;
}
//内部红黑树类,不止putTreeVal一个方法,还有许多,有兴趣可以查看Hashmap源码内部类TreeNode·
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>{
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
//寻找根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
final TreeNode<K,V> putTreeVal(
HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
/*
* map:当前Hashmap对象、tab:Hashmap对象中的table数组
* h:hash值
*/
Class<?> kc = null;
boolean searched = false;//标记是否被检索过
TreeNode<K,V> root = (parent != null) ? root() : this;//根节点
for (TreeNode<K,V> p = root;;) {//从根节点除开始遍历
int dir, ph; K pk;//dir为-1:放在左边、die为+1:放在右边
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))//要放进去key在当前树中已经存在了(equals来判断)。 直接返回该节点的引用 外边的方法会对其value进行设置
return p;
//说明进入下面这个判断的条件是 hash相同 但是equal不同。接下来就是解决hash冲突。
//自己实现的Comparable的话,不能用hashcode比较了,需要用compareTo
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//在左右子树递归的寻找 是否有key的hash相同 并且equals相同的节点
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
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进行设置
}
dir = tieBreakOrder(k, pk);//说明红黑树中没有与之equals相等的 那就必须进行插入操作。
}
/**
新节点安放好需要做一下一个操作:
1、对于红黑树要做的操作
1.1、父节点.left=新节点、或者父节点.right=新节点
1.2、新节点.parent=父节点
2、对于节点要做的操作
2.1、父节点.next=新节点
2.2、新节点.prev=父节点
**/
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {//获取父节点的子节点,并让子节点等于父节点
Node<K,V> xpn = xp.next;//获取三级节点
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);//newTreeNode(hash、key、value、prev)。创建一个新节点。然后根据hash值将新节点放于对应位置。
if (dir <= 0)
xp.left = x;//新节点位于二级节点的左边
else
xp.right = x;//新节点位于二级节点的右边
xp.next = x;//TreeNode.next=新节点
x.parent = x.prev = xp;//新节点.prev=父节点、新节点.parent=父节点
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
//moveRootToFront 方法是把算出来的root放到根节点上。
//balanceInsertion 对红黑树进行着色或旋转,以达到更多的查找效率
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||(d = a.getClass().getName().d =
(System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
return d;
}
}
查询
一、haha不冲突情况下:
1、获取数组中节点A时(发现hahs值相等,并且equal也相等),返回该节点A
二、hash冲突情况下(链表、红黑树):
1、获取到某一个节点A(发生Hash相等,但是equals不等)
2、链表:直接死循环,找到相等节点并返回该节点,或者没有找到该节点,返回null
3、红黑树:使用红黑树查询方法(不清楚,下回研究)
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//数组部位null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//第一张情况,不发生hash冲突
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//第二种情况,发生hash冲突
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;
}
面试题
HashMap数据结构和底层原理
HashMap的扩容方式?负载因子是多少?为什是这么多?
答:先创建一个新的Entry空数组,长度是原来的两倍,重新遍历员Entry数组,把所有的entry重新Hash到新数组中。负载因子也称扩容因子等于 0.75f ,因子太小,会导致频繁扩容,浪费资源,太大会增加Hash冲突。
HashMap的主要参数都有哪些?
HashMap是怎么处理hash碰撞的?
答:使用链表和红黑树来解决hash碰撞。
hash的计算规则?
答: ( h = key.hashcode())^(h >>> 16)
你提到了还有链表,为啥需要链表,链表又是怎么样子的呢?
为啥改为尾部插入呢?
答:因为之前的使用hsahmap在并发+扩容情况下,会造成扩容死循环。
什么时候resize呢?
答:第一次初始化+以后每一成扩容
扩容?它是怎么扩容的呢?
为什么要重新Hash呢,直接复制过去不香么?
答:因为数组长度改变,导致hash取下表时规则也改变,所有要重写Hash,而不是直接复制过去
说完扩容机制我们言归正传,为啥之前用头插法,java8之后改成尾插了呢?
但是你都都说了头插是JDK1.7的那1.8的尾插是怎么样的呢?
那我问你HashMap的默认初始化长度是多少?
答:16
你那知道为啥是16么?
答:1<<4
那为啥用16不用别的呢?
答:2的幂,这样是为了位运算的方便,位运算比算术计算的效率高很多,之所以选择16,是为了服务将key映射到index的算法。
为啥我们重写equals方法的时候需要重写hashCode方法呢?你能用HashMap给我举个例子么?
我记得你上面说过他是线程不安全的,那你能跟我聊聊你们是怎么处理HashMap在线程安全的场景么?
对上面面试题有自己的理解,那差不多就掌握了面试过程中的HashMap。如有不懂可以提出,我会解答。
扩容
用到这个方法有两处:初始化、添加元素后数组扩容。
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) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
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;
}
else if (oldThr > 0)
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
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) {
// 把每个bucket都移动到新的buckets中
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 { //链表优化重hash的代码块
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);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
红黑树数据结构
红黑树添加节点===插入节点与二查树插入节点一致(左边插入的节点值<根节点值,有边插入的节点值>根节点值)。
//Treemap类
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key);
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);//修复红黑树———>即:使不平衡的红黑树----》修改后满足红黑树五个性质。
size++;
modCount++;
return null;
}
插入节点后修复红黑树:染色解决方案、旋转解决方案、染色旋转解决方案。——————这就是数据结构的魅力————一个方法完成三件事。
红黑树节点——Entry属性:
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
return (p == null ? null: p.parent);
}
private void fixAfterInsertion(Entry<K,V> x) {//x为要新增的节点
x.color = RED;//红黑树新节点变为红色
while (x != null && x != root && x.parent.color == RED) {//节点不为空、不是根节点、x的父节点为红色
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {//判断父节点是否在爷节点左边
//父节点在爷节点左边
Entry<K,V> y = rightOf(parentOf(parentOf(x)));//获取叔节点
if (colorOf(y) == RED) {//如果叔节点是红色—》那么将父节点变为黑色,将叔节点变为黑色,并且将爷节点变为红色、并且让爷节点变为X,然后把爷爷节点看作是新增节点,继续朝上进行染色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {//叔节点是黑色
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);//左旋转
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));//右旋转
}
} else {//父节点在右边
Entry<K,V> y = leftOf(parentOf(parentOf(x)));//获取叔节点
if (colorOf(y) == RED) {叔节点是红色色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {//叔节点是黑色
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);//右旋转
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));//左旋转
}
}
}
root.color = BLACK;
}
红黑树的必须满足几个特性:节点必须是红色或者黑色、根节点必须是黑色、叶节点必须是黑色、红色节点必须有两个黑色儿子节点、任意节点到叶子节点,其中经过的黑色节点是相等的。