目录
基本组成结构
HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。Key 不允许重复出现,Value 随意。jdk 8 之前,其内部是由数组+单向链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑树。
大致的数据存储形式如下:
类的继承关系
/**
* HashMap继承了抽象父类AbstractMap
* 实现了Map(定义了一组通用的操作)
* 实现了Cloneable(可进行浅拷贝)
* 实现了Serializable(可序列化)
*/
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
基本成员属性
/**
* 底层存储容器
* Node类型的数组,每个Node元素都是一个链表的头结点,通过它可以访问连接在其后面的所有结点
*/
transient Node<K,V>[] table;
/**
* table的默认容量:16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* table的最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子,用于扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表转红黑树的阈值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树转链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 最小转红黑树的容量
* (就是说,即使链表长度达到转红黑树的阈值,但总容量没有达到该值,也不会转红黑树)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 键值总数
*/
transient int size;
/**
* 用于迭代过程中防止结构性破坏的标量
*/
transient int modCount;
/**
* 下一次扩容的阈值 threshold = capacity * load factor
*/
int threshold;
/**
* 负载因子
*/
final float loadFactor;
构造函数
/**
* 自定义初始化容量与负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
// 初始化容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始化容量大于最大默认容量则取最大默认容量为初始化容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子不能小于等于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// tableSizeFor返回大于等于initialCapacity的最小2的指数幂
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 自定义初始化容量
*/
public HashMap(int initialCapacity) {
// 负载因子取默认值0.75f
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 默认方式
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 将m中的所有元素添加至HashMap中
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
核心方法
put
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 计算key的hash地址
* 为了使hash值尽可能分散,将key的hash值的高32位与低32位进行异或运算,得到新的hash值
*/
static final int hash(Object key) {
int h;
// 如果key为null,默认在table[0]的位置,由此看出key是允许为null的
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1. 如果table还未被初始化,那么初始化它
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 如果为null,说明此索引位置并没有被占用,(n - 1) & hash:得到数组下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 3. 不为null,说明此处已经被占用,只需要将构建一个结点插入到这个链表的尾部即可
Node<K,V> e; K k;
// 3.1 如果当前结点key的hash值和将要插入的结点key的hash值相同,
// 且是引用同一个对象或equals方法为真,说明这是一次修改操作,覆盖value值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 3.2 如果p这个头结点是红黑树结点的话,在红黑树中查找
// 存在则更新value,返回原oldValue,不存在则作为新结点插入,返回null
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {// 3.3 如果不是前两种情况,遍历此链表,将构建一个结点插入到该链表的尾部
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果插入后链表长度大于等于 8
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;
}
}
// 如果e不是null,说明当前的put操作是一次修改操作,且e指向的就是需要被修改的结点
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 4. 如果添加后,数组容量达到阈值,进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* table太小,改成扩容操作
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// table太小,不到64,改为扩容操作
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);
}
}
put(K key, V value)方法总结:
1. 先判断table是否初始化,如果没有,则进行初始化。可见table真正初始化是在第一次put的时候。
2. 通过key的hash值计算出新增结点在数组中的位置。如果该位置为null,直接将其存入该位置。
前面通过hash()方法将key的hash值的高32位与低32位进行异或运算,得到新的hash值。这么做的目的是为了使hash值尽可能分散。
然后通过(n - 1) & hash结果得到数组下标,n为table的当前容量,下标范围应该是0~(n-1)范围内的整数。
以n=16为例,(n - 1) & hash可看作:1111 & 01001101010011010100110101001101
由此可以看出,为了让结果覆盖最多的可能,n-1的二进制值应该都为1,因此n的取值应该是2的指数幂,这也是table容量的取值为什么一定是2的指数倍的原因。
3. 如果该位置不为null,说明此处已经被占用,此时需要判断是覆盖还是插入。
3.1 如果当前结点key的hash值和将要插入的结点key的hash值相同,且是引用同一个对象或equals方法为真,说明这是一次修改操作,覆盖value值。
3.2 如果该位置的头结点是红黑树结点的话,在红黑树中查找,存在则更新value,返回原oldValue,不存在则作为新结点插入,返回null。
3.3 如果不是前两种情况,遍历此链表。在遍历过程中,若发现新结点与某个结点的key值相等,覆盖该结点,否则将新结点插入到该链表的尾部。如果插入后链表长度大于等于 8 ,执行treeifyBin(tab, hash)方法。
3.4 在treeifyBin(tab, hash)方法中需要注意,并不是直接将链表裂变成红黑树,而是先判断table是不是小于64,如果是则会进行扩容操作,只有table足够大了(>=64),才会转红黑树。
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;
// oldCap > 0 说明数组已经初始化完成,此处需要给旧数组扩容
if (oldCap > 0) {
// 如果容量达到极限将不再扩容,直接返回旧table
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果容量扩大两倍未达到极限,且容量不小于默认容量
// 将数组容量扩大两倍,阈值也扩大两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/* 数组未初始化,阈值大于0时
说明使用了构造函数 HashMap(int initialCapacity, float loadFactor)初始化
根据传入的容量initialCapacity计算出一个合适的容量暂存在阈值中 */
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;
// 旧数组为null则初始化,不为null则进行扩容
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
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断e在扩容后的索引是否变化
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;
}
}
}
}
}
// 无论是扩容还是初始化,都返回 newTab
return newTab;
}
resize()方法总结:
- 判断是初始化调用还是扩容调用,计算新的数组容量与下一次扩容的阈值,同时创建出新数组。
- 如果是初始化调用,直接返回新数组;如果是旧数组扩容,还需要将旧数组中的各个结点复制到新数组中,其中包括单个结点,链表结点与红黑树结点的复制。
旧结点在新数组中地址分配过程分析:
单个结点:根据 e.hash & (newCap - 1) 计算出在新数组中的位置。
链表结点:由于扩容后,容量会左移一位,因此可以根据 e.hash & oldCap 来判断扩容后链表元素的 hash 值参与计算的部分是否有变化,无变化的部分在原位置 newTab[index],有变化的部分在扩容后的新半区 newTab[index + oldCap]。
e.hash & oldCap 是如何以判断 hash 值是否变化的?
以oldCap = 16 为例:
index = e.hash & (oldCap - 1) = 010010......1001 & 1111 = 1001
无变化情况: e.hash & oldCap = 010010......01001 & 10000 = 0
有变化情况: e.hash & oldCap = 010010......11001 & 10000 = 1
即通过判断hash值新加入比较的一位是0还是1。
红黑树结点:与链表方式类似,同样将树分裂出一部分到newTab[index + oldCap],如果分裂后的树结点过少,则以链表形式重组。
get
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;
// 当table不为null时,且key的hash值所在坐标上不为null时
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;
}
get(Object key)方法总结:
- 当数组不为空时,计算传入参数key的hash值,定位其在数组中的位置。
- 如果该位置不为null,先通过 == 或 equals方法比较key与该位置头结点的key是否匹配,匹配则返回结点的value值。
- 如果是链表或红黑树,则通过遍历的方式查找匹配的结点,找到后返回结点的value值。
- 如果以上情况都没有匹配的结点,直接返回null。
remove
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;
// 当table不为null时,且key的hash值所在坐标上不为null时
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;
// 查找要删除的结点,找到后用node指向该结点
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为要删除的结点,再判断是否要匹配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);
// 如果node是头结点,直接将node.next设为头结点
else if (node == p)
tab[index] = node.next;
// 如果node是中间结点,node的前结点p的next结点指向node的next结点
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
remove(Object key)方法总结:
- remove方法就是查找 + 删除的过程,首先要根据key值找到要删除的结点,过程与get方法一致。
- 找到要删除的node结点后,如果是红黑树结点,直接调用红黑树的删除方法;如果是一个头结点,那么用node.next 结点代替它作为头节点存放在 table[index] 中;如果是链表的中间结点,使node的前一结点的next直接指向node.next 结点即可。
- 成功删除node后,remove方法会返回node.value值;如果未找到要删除的结点,直接返回null。
entrySet
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
/**
* HashMap的内部类
* 实现了Iterable接口,因此可用来迭代
*/
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
// foreach遍历EntrySet的时候时间上是会遍历table[]
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
// 通过modCount禁止多线程并发写操作
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
entrySet()方法总结:
- entrySet返回了EntrySet对象,EntrySet是HashMap的内部类,实现了Iterable接口,它可以直接操作HashMap底层的数组table,因此可以对HashMap的键值对集合进行迭代。
- keySet、values与entrySet作用相似,keySet返回了HashMap的key的集合,values返回value的集合。
- 另外,由于HashMap是非线程安全的,因此在所有迭代操作过程中,都有modCount变量来限制并发下的结构性破坏操作。
HashMap常见面试题
1. HashMap的原理,内部数据结构?
底层使用哈希表(数组 + 链表),当链表过长会将链表转成红黑树以实现O(logn)时间复杂度内查找。
2. 讲一下HashMap中put方法的过程。
i. 对Key求Hash值,然后再计算下标;
ii. 如果没有碰撞,直接放入桶中;
iii. 如果碰撞了,以链表的方式链接到后面;
iv. 如果链表长度超过阈值(TREEIFY_THRESHOLD == 8),并且总容量超过64,就把链表转成红黑树;
v. 如果节点已经存在就替换旧值;
vi. 如果桶满了(容量 * 负载因子),就需要resize。
3. HashMap中hash函数是怎么实现的?还有哪些hash的实现方式?
i. 高16bit不变,低16bit和高16bit做一个异或运算;
ii. (n - 1)& hash 得到下标;
4. HashMap怎样解决冲突,讲一下扩容过程,加入一个值在原数组中,现在移动了新数组,位置肯定改变了,那是什么定位到在这个值新数组中的位置?
i. 将新节点追加到链表上;
ii. 容量扩充为原来的二倍,然后对每个节点重新计算哈希值;
iii. 这个值只可能在两个地方,一个是在原下标的位置,另一种情况时在下标为<原下标 + 原容量>的位置。
5. 抛开HashMap,hash冲突有哪些解决办法?
i. 链地址法(HashMap使用了该方式);
ii. 再哈希法(产生冲突时计算另一个hash函数地址,直到没有冲突为止);
iii. 开放定址法。
6. 针对HashMap中某个Entry链太长,查找的时间复杂度可能达到O(n),如何优化?
链表转为红黑树,JDK 8已经实现。