HashMap源码解析
- HashMap的成员变量和构造方法(本文源码基于JDK 1.8)
public class HashMap<K, V> {
/**
* 默认容量16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* Hashmap数组节点上的链表转为红黑树的的第一个条件,链表节点数达到8个
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 链表节点数小于6个时,从红黑树转换为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 链表转为红黑树的第二个条件,与TREEIFY_THRESHOLD对应,最小的链转树的数组大小。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* Node<K,V>数组
*/
transient Node<K, V>[] table;
/**
* key-value映射的个数
*/
transient int size;
/**
* 记录hashmap节点修改次数,这个字段用于保障在for循环遍历hashmap时,
* 不可以对hashmap里面的数据发生结构性改变,如删除其中一个key-value,
* 会导致fast-fail【抛出ConcurrentModificationException】,
* 正确的方式是使用迭代器遍历删除。
*/
transient int modCount;
/**
* 阈值 =(容量 * 加载因子)
*/
int threshold;
/**
* 加载因子
*/
final float loadFactor;
/**
* 创建一个初始容量为initialCapacity,加载因子为loadFactor的空HashMap
*/
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);
}
// 创建一个初始容量为 initialCapacity ,加载因子为 0.75 的空 HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 创建一个初始容量 16 ,加载因子为 0.75 的空 HashMap
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 创建一个 HashMap ,其加载因子为 0.75 , key 和 value 为 m 的 key 和 value
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
}
我们最常使用的是无参的构造方法,这个构造方法里面给 loadFactor 赋了初始值 0.75。在你调用 put() 方法往 HashMap 中插入元素的时候会调用 resize() 方法,在这里会初始化 threshold,table 数组也是在 resize() 方法中初始化的,后面我们会提到。
如果你使用带 initialCapacity 参数的构造方法,里面会调用 tableSizeFor(initialCapacity) 来计算 threshold ,代码如下:
static final int tableSizeFor(int cap) {
int n = cap - 1;
// >>> 代表无符号右移
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法是用来计算出一个大于或等于 cap 的最小的 2 的幂。它的实现步骤就是把最高位的 1 传给低位,比如传入 cap=9 ,那么 n = 0x1000,n 右移 1 位与自己进行或运算后 n = 0x1100,n 右移 2 位与自己进行或运算后 n = 0x1111,最终通过 tableSizeFor() 计算出来的结果就是 16。
- HashMap的Node节点
HashMap 是链表的时候,使用 Node 节点来存储键值对, Node 是 HashMap 的静态内部类,有 hash , key , value , next 四个属性,代码如下:
public class HashMap<K,V>{
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//构造方法
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
}
转成红黑树后使用 TreeNode 来存储键值对,代码如下:
public class HashMap<K,V>{
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;
//构造方法
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
}
- HashMap 中的 put() 方法
public class HashMap<K, V> {
/**
* 将 value 与 key 在 map 中关联起来,如果 map 中存在与 key 关联的 value ,
* 旧 value 将被替换,返回值为旧 value
*/
public V put(K key, V value) {
// 通过 hash(key) 取值
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;
//初始化tab=table,初始化n=tab.length,table为null时执行resize()初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过(n - 1) & hash 拿到数组下标i,p为tab[i]对应的节点,
//也是链表的头节点,如果为null,新建Node
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果p不为null
else {
Node<K, V> e;
K k;
//如果头结点的hash和key与插入节点相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果头结点是红黑树
else if (p instanceof TreeNode)
//调用红黑树的putTreeVal()方法
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
//如果头结点是链表
else {
for (int binCount = 0; ; ++binCount) {
//p.next赋值给e,p.next == null即表示遍历到了链表尾部
if ((e = p.next) == null) {
//在链表尾部插入新Node
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//链表长度为8时,调用treeifyBin()方法
treeifyBin(tab, hash);
//退出循环
break;
}
//节点的hash和key与插入节点相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//退出循环
break;
//遍历
p = e;
}
}
//map中已经存在对应的key
if (e != null) { // existing mapping for key
V oldValue = e.value;
//替换旧value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回旧value
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);
}
}
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
return new Node<>(hash, key, value, next);
}
//空方法
void afterNodeInsertion(boolean evict) {
}
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) {
//如果旧的容量已经超过MAXIMUM_CAPACITY = 0x40000000,
//则不进行扩容操作,将当前阈值赋值为Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
//Integer.MAX_VALUE = 0x7fffffff
threshold = Integer.MAX_VALUE;
return oldTab;
}
//newCap是oldCap的2倍,newThr是oldThr的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//如果旧的容量等于0,旧的阈值大于0
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//旧的容量和阈值都等于0
else { // zero initial threshold signifies using defaults
//调用HashMap的默认构造函数走这里初始化
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数组
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;
//如果是红黑树,执行split()方法
else if (e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
//链表重新hash
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;
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;
}
/**
* 将node结点拆分成lower tree bin和upper tree bin,
* 如果发现太小会转回链表
*/
final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) {
TreeNode<K, V> b = this;
// Relink into lo and hi lists, preserving order
//重新链接到 lo 和 hi 两个链表,保持以前的顺序
TreeNode<K, V> loHead = null, loTail = null;
TreeNode<K, V> hiHead = null, hiTail = null;
//lc是lo链表的元素个数,hc是hi链表的元素个数
int lc = 0, hc = 0;
for (TreeNode<K, V> e = b, next; e != null; e = next) {
next = (TreeNode<K, V>) e.next;
e.next = null;
//bit为容量,添加到lo链表
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
//添加到hi链表
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//如果lo链表的元素个数小于等于6,退化成链表
//并插入到新数组 tab[index] 的位置上,index是当前红黑树所在旧数组坐标
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
//如果hi链表的元素个数小于等于6,退化成链表
//并插入到新数组 tab[index + bit] 的位置上,index是当前红黑树所在旧数组坐标
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
}
通过以上代码可以得出以下结论:
- table 的初始化在 resize() 方法中。
- HashMap 在执行 put() 方法时,如果发现链表长度大于或等于 8 ,并且数组长度大于或等于 64 时,会将链表转成红黑树。
- 如果红黑树的TreeNode个数小于或等于 6 ,会从红黑树转回链表。
- HashMap 中的 remove() 方法
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
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;
//如果头结点就是要remove的节点
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!=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]指向下一个节点
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
}
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K, V> first = (TreeNode<K, V>) tab[index], root = first, rl;
TreeNode<K, V> succ = (TreeNode<K, V>) next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
//如果红黑树根(root)为空,或者根的右子树为空,或者根的左子树为空,
//或者根的左子树的左子树(root.left.left)为空,
//都会发生红黑树退化成链表
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K, V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K, V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red;
s.red = p.red;
p.red = c; // swap colors
TreeNode<K, V> sr = s.right;
TreeNode<K, V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
} else {
TreeNode<K, V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
} else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K, V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // detach
TreeNode<K, V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
}
由以上代码可以得出以下结论:
- HashMap在执行remove()方法时,如果发现红黑树根为空、根的左子树或右子树为空,或根的左子树的左子树为空,都会发生红黑树退化成链表的情况。
HashMap面试题
- 讲讲 HashMap 的数据结构和底层原理。
HashMap 是由数组和链表组合成的数据结构(Java 8 加入了红黑树),里面存储了 Key-Value 键值对,在 Java 7 叫 Entry,在 Java 8 中叫 Node 。在插入的时候会根据 key 的哈希算法来计算插入的位置,但是有一定概率计算出来的哈希值是一样的,值一样的情况我们就放到一个链表上。
- 数据在插入链表的时候是怎么插入的?
在插入之前先对插入的节点的key通过hash(key)取值,然后通过hash(key) & 数组长度(length -1)获取它在数组中的下标i,最后在Node[]数组中查找Node[i],如果Node[i]为null,直接插入;如果有值,遍历对应位置的链表,如果有Node的hash值与之相等并且key值也相等,则替换Node的value并返回,否则插入链表的末尾。
Java 8之前是头插法,会插入链表的头部,原有的值就顺序往后推,因为写这个代码的作者认为后插入的值被查找的可能性更大一点,插入头部能提升查找的效率;Java 8之后用尾插法。
- 为什么Java 8之后采用尾部插入?
原因是头插法在多线程插入数据的情况下会造成环形链表(Infinite Loop),使用尾插法在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
那是不是意味着Java 8就可以把HashMap用在多线程中呢?
不是的,即使不会出现死循环,但是通过源码可以看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。
- 链表是怎么扩容的?扩容因子为什么是0.75?
链表扩容主要分为2步:
- 扩容:创建一个新的Node空数组,长度是原数组的2倍。
- ReHash:遍历原Node数组,把所有的Node重新Hash到新数组。
- 为什么要重新Hash(rehash)?为什么不直接复制过来?
Hash的公式: index = HashCode(Key) & (Length - 1),扩容后Length已经变了,计算出来的index会发生改变,原来在一个链表下的数据执行rehash后很大概率不会还在一个链表下。
- HashMap的默认大小为什么是16?
index的计算公式为:index = HashCode(Key) & (Length- 1),Length初始值为16,16是2的4次方,后续扩容也都是2的指数次幂,16-1的二进制是1111,这样hashcode与之进行&运算就是hashcode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的,这是为了实现均匀分布,最大程度地避免哈希碰撞。同时,在计算机中,位运算比算数运算效率高很多,所以在源码中使用的是DEFAULT_INITIAL_CAPACITY = 1 << 4。
- 为什么重写equals()方法的时候需要重写hashCode()方法?
HashMap的put()方法会使用key的hashCode()方法和equals()方法来协同确定元素的位置:先通过hashCode()方法确定在数组中的index,再通过equals()方法来确定在链表上的位置。如果只重写了equals()方法但是没有重写hashCode()方法,会直接执行Object中的hashCode()方法,而Object中的hashCode()方法对比的是两个对象的内存地址,不同的对象返回结果是false,后面的equals()方法也不用执行了,直接返回的结果就是false。
运行如下示例:
public class Test {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<>();
String name = "Jim";
int age = 20;
Person person1 = new Person(name, age);
map.put(person1, name);
Person person2 = new Person(name, age);
map.put(person2, name);
Iterator<Person> keyIterator = map.keySet().iterator();
while (keyIterator.hasNext()) {
Person key = keyIterator.next();
String value = map.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}
}
static class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 引用相等返回 true
// 如果等于 null,或者对象类型不同返回 false
if (o == null || getClass() != o.getClass()) return false;
// 强转为自定义 Person 类型
Person person = (Person) o;
// 如果 age 和 name 都相等,就返回 true
return age == person.age && Objects.equals(name, person.name);
}
// @Override
// public int hashCode() {
// return Objects.hash(name, age);
// }
}
}
打印如下:
Key: com.example.test.Test$Person@610455d6, Value: Jim
Key: com.example.test.Test$Person@511d50c0, Value: Jim
通过以上代码可以看到虽然person1和person2的name和age一样,但是HashMap在插入person2时不会覆盖person1,所以会有2条打印。但是如果把hashCode()方法的注释打开,就只会有一条打印了。
我们经常使用Integer或String作为HashMap的key,这些类里面已经对hashCode()方法和equals()方法进行了重写,所以不需要我们额外处理。但是如果你使用自定义的类作为HashMap的key就需要自己重写hashCode()方法和equals()方法。
- HashMap是线程不安全的,怎么解决线程不安全的场景呢?
一般我们使用Collections.synchronizedMap(Map)创建线程安全的map集合、使用HashTable或者ConcurrentHashMap都可以用来解决线程不安全场景,不过出于线程并发度的原因,我都会舍弃前两者使用最后的ConcurrentHashMap,他的性能和效率明显高于前两者。
-
hash的计算规则?
-
为什么引入红黑树?
为了减小查找和插入的时间复杂度,红黑树的引入将原本O(n)的时间复杂度降低到了O(logn)。
- 什么时候转换成红黑树,删除元素后会回到链表吗?
HashMap在执行put()方法添加元素时,如果发现链表长度大于或等于8,并且数组长度大于或等于64时,会将链表转成红黑树。
在扩容resize()时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
在移除元素remove()时,removeTreeNode()方法会检查红黑树是否满足退化条件,如果红黑树根为空,或者根的左子树/右子树为空,或者根的左子树的左子树为空,都会发生红黑树退化成链表。
感谢与参考: