这里聊一下HashMap:
HashMap底层数据结构:
HashMap1.7之前数据结构是数组+链表
HashMap1.8之后数据结构加了红黑树(是用来处理hash冲突的)
HashMap1.7之前put的时候使用的是头插法
HashMap1.8之后put的时候使用的是尾插法
什么时候初始化HashMap?
在第一次put操作的时候进行初始化。实际上put方法里面调用putVal方法,在putVal方法里面调用了resize方法,在resize方法里面进行初始化map操作。
初始化数组:默认初始化容量是16。
也可以在定义hashmap的时候有参数,在里面自定义容量;当自定义容量的时候,如果不是2的指数次幂,将会强转至大于自定义的最接近的2的幂值。
获取key所在桶位:不是通过取模数来定位索引的,而是与运算 h & (length - 1).
WHY?因为数据量大的时候,扩容次数增加,这时候位运算效率高于取模。并且因为hashmap的length是2的次方数,所以-1后导致二进制表示低位都是1;(例如16-1:1111)容易计算。
为什么初始容量要是2的幂次方?
主要有两个原因:
HashMap安全吗?
1.8之前HashMap不安全,当线程数多的时候进行put操作可能会造成循环问题,造成数据丢失
1.8进行了优化:用HiHead,HiTail,LoHead,LoTail四个指针进行均匀分割
将当前位置的每一个key进行和旧的大小与:只能是0或者旧size,结果是0就将lo指向,否则就指向Hi。然后将低的直接转移至新的,高的转移到低+旧大小,理论是均匀的。
但是如果是多线程情况下:有可能两个线程同时调用put方法,将一对key-value要放入散列表中,有可能是放入到一个桶位中,在一个位置放入了两次数据,导致不知道那个线程put成功,造成数据覆盖问题。
要实现安全就要用到ConcurrentHashMap:参考我的另外一篇文章ConcurrentHashMap详解
JDK1.7的HashMap死循环问题?
主要原因是JDK1.7的HashMap使用的是头插法。当一个线程扩容完进行数据迁移的时候,另外一个线程进入,完成了扩容和迁移;然后第一个线程进行数据迁移的时候数据是逆序的了(头插法的坏处,迁移一次会造成一个桶位的数据逆序);然而第一个线程取的时候是顺序取,而实际位置是逆序,等循环一遍就会造成死循环问题。
HashMap的put操作
先要进行定位:要找到放在hashmap中的那个桶位。这块主要是给hash(key)后再进行扰动函数处理后得到位置:让key的hash值的高16位也参与运算,使得散列性更好。
put的时候有下面四种情况:
下面源码来自于jdk1.8的HashMap源码:
//可以看到如果key是null的话,就放在了map中的0位置
// (h = key.hashCode()) ^ (h >>> 16):将原来的hash值和右移16位后,并且和原来hash值进行异或运算
static final int hash(Object key) {
int h;
//是一个扰动函数;使得hash值的高16位也参与到运算,更加散列
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//进行put操作,实际是调用putVal方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//延迟初始化,第一次调用put的时候加载。
if ((tab = table) == null || (n = tab.length) == 0)
//实际就是用resize方法进行初始化,如果没有就从0扩容到16,如果有就扩容到两倍
n = (tab = resize()).length;
//第一种情况,对应位置是空,直接添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//第二种情况,找到了要插入的key和这里的key相同的,要replace
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) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//第一个条件:8-1=7;实际是当7的时候进行树化(因为count从0开始)
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 = e;
}
}
//这里是replace操作,将新value放入即可
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //记录表结构修改的次数
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//树化方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//第二个条件:如果没有64个就进行resize扩容操作
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//说明满足两个条件,
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;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
HashMap的get操作
get是从hashmap中找到key经过hash和扰乱后对应的桶位查找对应的value;主要有三种情况:
源码:来自于jdk1.8的HashMap源码
//get操作
public V get(Object key) {
Node<K,V> e;
//这里hash就是找到对应的桶位,具体hash算法看上面的源码
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;
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;
}
HashMap的remove操作
传入key参数,从hashmap中将对应的节点移除;remove方法主要是先找到对应位置,在进行移除操作;主要有三种情况:
源码:来自于jdk1.8的hashmap源码
public V remove(Object key) {
Node<K,V> e;
//调用核心方法
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//查询位置。并且节点就是要删除的节点
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 {
//第三种情况,要遍历整个链表,直到找到位置就break
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//删除节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
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;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
HashMap的扩容机制(resize)
扩容要遍历每个桶位,在进行操作,主要有三种种情况:
链表转移到新散列表过程(链表迁移过程):
将第一个是0的指向loHead,遇到其他等于0的就指向loTail,一直指并且loTail后移;将第一个是1的指向hiHead,遇到其他等于0的就指向hiTail,一直指并且hiTail后移;
将loHead到loTail放到新散列表的原来位置上,将hiHead到hiTail放到新散列表原来位置+原来散列表长度位置即可
源码:来自于jdk1.8的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;
//旧散列表容量大于0
if (oldCap > 0) {
//旧散列表容量已经大于最大容量,就直接返回,不进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//就散列表容量左移一位(扩大一倍)还小于最大容量并写旧的容量大于默认容量16,就将旧的阈值左移一位(扩大一倍)作为新的阈值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//旧散列表容量等于0的情况,说明散列表是空的
//旧的阈值大于0,在new HashMap的时候传入参数的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//阈值和容量都等于0,就进行赋值操作
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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)
//将节点放到新散列表的位置 通过hash & (newCap - 1)计算位置
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;
//相与后是0:就是低位链表
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;
}
为什么要引入红黑树?
因为就是要解决链化过长的问题。当hash一直冲突的时候,就会造成某个位置链化过长,查询效率过低。引入红黑树,使得get操作效率提升。
HashMap真的是从8开始链表转红黑树吗?
必须满足两个条件:
链表转红黑树:当数组长度<64,优先扩容;当>=64,在判断是否有其中的节点链化达到树化阈值(默认是8),实际链表长度是9.当满足两个条件就给对应位置进行树化为红黑树。
这些都是看源码获取的,要是有错误请指教。
红黑树特性
一般插入节点的时候都默认是红色的,因为如果是黑色的话,哪整个红黑树都是黑色节点,也满足条件,哪不就还是普通的二叉树。
红黑树转换
红黑树的转变主要是三种方式:变色,左旋,右旋。
转换步骤如下: