1. HashMap底层数据结构
- 数组
- 链表(单向链表)
- 红黑树
2. 需要理解什么
- hash冲突如何解决
- 如何计算key的hash值
- 什么时候扩容、扩多大
- 数组的长度为什么是2的N次幂
- 查找、插入、扩容的过程
2.1 数据结构
-
通过该图我们可以知道,当向map中插入一个值,首先是需要根据key生成一个hash值,根据hash值来确认存储在数组的什么位置。
如果发生hash冲突就转换成以链表的形式存储,当链表的长度大于8并且数组的长度大于64时,转换成红黑树进行存储。
基本属性默认值:
// 默认容量大小 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表长度
static final int TREEIFY_THRESHOLD = 8;
// 最小转换为树的数组长度
static final int MIN_TREEIFY_CAPACITY = 64;
// Hash数组(在resize()中初始化)
transient Node<K,V>[] table;
// 容量阀值 计算规则 = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR
int threshold;
table数组中存放的时Node对象,Node时HashMap的一个内部类,表示一个key-value
// Node对象
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // hash值,根据key生成
final K key; // key
V value; // value
Node<K,V> next; // 如果hash值在数组中存储着了,那么则生成链表,指针指向下一个
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
备注: 数组初始化不是在构造器中初始化,在resize(扩容)方法里面进行初始化的。
设置阀值
在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);
// 负载因子 0.75f
this.loadFactor = loadFactor;
// 计算阀值
this.threshold = tableSizeFor(initialCapacity);
}
// 根据初始容量计算阀值
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;
}
如何解决hash冲突
hashMap新增时,存储到指定的数组下标所在的位置,通过的是 hash & (length - 1),因此增加了hash的随机性,从而减少了hash冲突,归根结底,这些功劳都应该归根于设计时将长度设计为2的幂次方。
扩容
hashMap扩容每次都是创建一个新的数组,把原数组中的内容重新映射到新数组上。
具体步骤:
根据流程图,可以知道:
- 首先判断table是否初始化,如果初始化了,则将容量,阀值都扩大到原来的2倍,这时,需要重新计算key-value在table中的位置。
- 如果没有初始化,并且我们的阀值是大于0的,那么说明了调用了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;
//1、若oldCap>0 说明hash数组table已被初始化
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}//按当前table数组长度的2倍进行扩容,阈值也变为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
//2、若数组未被初始化,而threshold>0说明调用了HashMap(initialCapacity)和HashMap(initialCapacity, loadFactor)构造器
else if (oldThr > 0)
newCap = oldThr;//新容量设为数组阈值
else { //3、若table数组未被初始化,且threshold为0说明调用HashMap()构造方法
newCap = DEFAULT_INITIAL_CAPACITY;//默认为16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//16*0.75
}
//若计算过程中,阈值溢出归零,则按阈值公式重新计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//创建新的hash数组,hash数组的初始化也是在这里完成的
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果旧的hash数组不为空,则遍历旧数组并映射到新的hash数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//GC
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 {
//rehash————>重新映射到新数组
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/*注意这里使用的是:e.hash & oldCap,若为0则索引位置不变,不为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;
}
2.2. HASHMAP put方法
步骤解析:
-
table为空时,通过扩容的方式初始table的大小 (初始容量 * 0.75 = 实际容量大小)
-
通过计算hash值求出下标后,如果没有存在hash冲入,则新增Node节点对象
-
如若发生hash冲突,遍历链表查找需要插入的key是否存在,如果存在,则替换掉原来的value
-
如若不存在,则在尾部进行插入,并且判断当前链表长度是否大于8,
-
如若大于8,则将链表转换为红黑树进行存储
-
判断key-value的数量是否大于等于阀值
-
如若是,则进行扩容操作
源码如下:
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; // 1、 判断当前数组是否为空,如若为空,则调用resize方法,生成初始容量 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 初始容量 ,计算: 默认容量 * 负载因子 = 阀值 (让阀值作为当前table的容量) if ((p = tab[i = (n - 1) & hash]) == null) // 数组中的下标位置: 计算, hash & (n-1) = p (指针) tab[i] = newNode(hash, key, value, null); // 如果为null,则生成一个新的Node节点 else { // 如果不为null,插入的key-value已经存在 Node<K,V> e; K k; // 如果第一个节点就是要插入key-value,则让p执行第一个节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果p是TreeNode类型,则调用红黑树的的插入操作 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 对链表进行遍历,用计算长度 for (int binCount = 0; ; ++binCount) { // 如果不等相等,则在链表的尾部插入Node if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 判断链表长度是否大于等于8 treeifyBin(tab, hash); // 转换成红黑树存储数据 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 当前key存在 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; }
2.3. HASHMAP GET方法
步骤解析:
- 根据key生成hash值,查找数据
- 默认获取第一个Node,判断第一个Node节点中的key是否等于需要的key
- 是,则返回,不是,则将指针下移
- 判断当前Node是否为红黑树中的节点,如若是,则调用红黑树查找对应的Node
- 如若不是,则向后进行遍历查询
- 如若当前数组为null,则直接返回null
源码解读:
public V get(Object key) { Node<K,V> e; // 根据key生成hash值 return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; // 指向数组的hash数组 Node<K,V> first, e; // 指向hash数组中的第一个Node int n; // 数组长度 K k; // 1、判断第一个节点Node中是否存在数据 if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) { // 存在数据,判断key是否相等 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) // 相等,则返回 return first; // 第一个key与查找的key不相等 if ((e = first.next) != null) { // 判断第一个Node是否为TreeNode类型的树据 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); } } // 不存在数据,返回null return null; }