1、HashMap源码分析
JDK 1.8之前HashMap的底层是数组+链表结合在一起,也就是链表散列
JDK 1.8中HashMap的底层是数组+链表+红黑树,使用红黑树提高了查找效率
在分析源码之前,先来简单介绍以下 hashCode()在 HashMap中的作用:
HashMap中使用节点数组来进行存储(这个节点可能代表一个链表,也可能代表一个红黑树),添加键值对时,通过key的 hashCode()经过扰动函数(让高位也参与计算,避免频繁的哈希冲突),然后使用 (n - 1) & hash(n是数组的长度,n是2的次幂,所以 hash & n = hash & (n - 1))确定该元素应该添加到数组的哪个位置(索引),如果没有找到对应的节点(为null)则使用这个key value创建节点兵添加到数组对应的索引位置,否则使用key的equals方法进行比较,如果equals返回true则覆盖,否则将其添加到链表或红黑树对应的位置
JDK 1.8的扰动函数hash方法的源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK 1.7的扰动函数hash方法的源码:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK 1.7的扰动函数扰动了4次,而JDK 1.8中只扰动了1次,所以很明显JDK 1.8的扰动函数效率更高
HashMap在从JDK 1.7到1.8最大的变化在于如果解决哈希冲突,即当key计算出来的索引位置已经存在了元素应该如何处理;1.7中使用链表来解决;而1.8中则使用链表加红黑树来解决。
1、类的属性
// 默认的容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 一个空的哈希表(Entry数组)实例
static final Entry<?,?>[] EMPTY_TABLE = {};
// 哈希表(Entry数组)在必要的时候会扩容,长度必须为2的次幂
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// map中包含的key-value映射的数量
transient int size;
// 临界值,到达临界值时就需要进行扩容,threshold = capacity * loadfactor
int threshold;
// 负载因子
final float loadFactor;
// map修改的次数
transient int modCount;
// 一个默认的阈值,当一个键值对的键是String类型时,
// 且map的容量达到了这个阈值,就启用备用哈希(alternative hashing)
// 备用哈希可以减少String类型的key计算哈希码(更容易)发生哈希碰撞的发生率。
//该值可以通过定义系统属性jdk.map.althashing.threshold来指定。
// 如果该值是1,表示强制总是使用备用哈希;如果是-1则表示禁用。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
loadFactor:加载因子 loadFactor是控制数组存放数据的稀疏程度,loadFactor越接近1,数组中存放的数据(entry)也就越多,数据就越密集;反之数据越稀疏。 loadFactor太大会导致查询效率变低;loadFactor太小又会导致数据分散,浪费了很多空间。所以loadFactor有一个默认值0.75f,是官方认为比较好的一个值。 初始的容量为16,此时临界值threshold = 16 * 0.75 = 12,也就是说当元素的数量达到12时就需要进行扩容,扩容的过程涉及到了rehash、复制数据等操作,非常消耗资源,所以应当尽量避免扩容操作。正确的做法是,如果能够确定大致需要存储的数量,就在构造HashMap对象时给出指定的容量!
2、节点类源码分析
Node节点源码如下:
static class Node<K,V> implements Map.Entry<K,V> {
// 缓存哈希值,这样再次使用到哈希码时就不需要调用hashCode方法进行计算了
final int hash;
// key
final K key;
// value
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;
}
// key 和 value的getter方法
public final K getKey() { return key; }
public final V getValue() { return value; }
// toString,输出这个Node对象会打印key=value的格式
public final String toString() { return key + "=" + value; }
// Node对象的hashCode实现:key的hashCode和value的hashCode做亦或运算
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 修改value的方法,返回修改前的值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// equals方法,可以看到equals方法中使用到的field,在重写hashCode时也必须使用到
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;
}
}
Node节点可以作为链表的结点
树节点的源码如下:
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; // 是否为红色,这是一个红黑树的节点
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 返回包含该节点的根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// 其他方法省略了
}
3、构造方法
// 指定初始容量和加载因子
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;
// 初始threshold为大于等于initialCapacity的最小2的次幂
this.threshold = tableSizeFor(initialCapacity);
}
// 指定初始容量,使用默认加载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 默认构造函数,负载因子为2的次幂
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 使用另一个Map来构造HashMap,调用putMapEntries方法来实现
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// 集合的大小
int s = m.size();
if (s > 0) {
// 判断table是否已经初始化
if (table == null) { // pre-size
// 如果没有初始化,s为m的元素个数
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 如果计算得到的t(实际需要的容量)大于threshold则初始化threshold
if (t > threshold)
threshold = tableSizeFor(t);
}
// 如果table已经初始化,且m的元素个数大于阈值,则进行扩容处理
else if (s > threshold)
resize();
// 将m中的所有元素添加到HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
可以看到,几个构造方法中都只是初始化loadFactor和threshold的值(默认构造方法threshold也没有初始化),并没有初始化table数组
4、put方法
HashMap中提供了put方法添加或更新元素,其中调用了putVal方法,但是这个方法是私有的 put方法源码:
public V put(K key, V value) {
// 内部调用了putVal方法将指定键值对添加或更新到HashMap中
return putVal(hash(key), key, value, false, true);
}
// 扰动函数,避免减少哈希冲突
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* hash: key经过扰动函数计算出来的hash值
* key: 键
* value: 值
* onlyIfAbsent: 如果为true,则不会改变已有的值
* evict: 如果为false,则table正在创建模式中
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table为null或者table的长度为0,则执行扩容操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 使用 n - 1 & hash求出应该出现在哈希表中的索引,判断此处是否存在
// 如果不存在则新建node节点插入到索引位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 如果存在则需要进一步判断了
Node<K,V> e; K k;
// 如果索引位置节点的hash值等于hash(key的hash)且索引节点的key和key是一个对象
// 或者索引key不为空且key和索引的keyequals返回true,则e指向p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 红黑树处理
else if (p instanceof TreeNode)
// 将key-value添加到该索引节点对应的红黑树中并返回更新或者新增的树节点对象给e
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);
// 如果此时链表中的元素已经达到树化的临界值,则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// todo: 树化方法的分析
treeifyBin(tab, hash);
break;
}
// e现在是当前正在遍历的节点的下一个节点(不为空)
// 如果链表中存在hash值相同,或者key为同一个对象或者key的equals返回true
// 则跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// p移动到下一个节点
p = e;
}
}
// 此时如果e不为null,则说明存在对应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);
// 返回null表明是插入节点,所以没有之前的值
return null;
}
现在使用语言来描述以下putVal方法的执行流程:
- 判断table是否为null或者长度为0
- 是则执行resize()方法扩容
- 否则继续执行
- 通过key的hashCode()经过扰动函数计算出来的hash找到对应的索引,判断索引位置节点是否存在
- 不存在,则直接插入节点
- 存在,则进一步判断是否为树节点
- 树节点,键键值对添加到对应的红黑树中,插入返回null,更新返回更新的树节点对象到e
- 链表节点,遍历链表节点,过程中如果存在key相同(hash、同一个对象、equals()),则更新这个值,没找到则在链表尾部插入节点,插入后判断是否达到树化的临界值,则尝试将链表转化为红黑树
- 将链表转化为红黑树时,如果此时table数组的长度小于默认最小树化长度64,则执行扩容操作,而非将 链表转化为红黑树
- 如果链表或者红黑树中key存在,则将这个节点赋值给e,此时判断e是否为null,不为null说明e就是要更新的节点对象,此时更新这个节点的值并返回旧值
- 如果e为null,说明是插入节点操作,此时插入操作已经完成,需要判断是否超过了临界值,超过了则进行扩容操作,由于是插入操作没有旧值,所以返回null
扩容的resize()方法会在后面介绍
resize方法源码: **
5、get方法
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;
// 根据hash找到该key应该在是索引位置,如果该索引位置的节点不为空,将其赋值给first
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果first的hash和hash相等或者first的key和key的equals返回true,则返回first节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 当first不是一个单独的节点时
if ((e = first.next) != null) {
// 如果first是树节点实例,则使用红黑树的查找方法,查找对应的节点
if (first instanceof TreeNode)
// todo: 红黑树查找方法的分析
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);
}
}
// 如first为null,则返回null表示没有查找到
return null;
}
get方法思路分析:
- 首先根据通过key的
hashCode()经过扰动函数处理得到的hash,在根据(n - 1) & hash(n为数组长度)得到应该散列到的索引 - 将数组中索引位置的值赋值给first,判断first是否为空
- 是,则返回null
- 否,则继续判断first是否为需要查找的节点
- 是,则返回first节点
- 否,继续判断first是否有下一个节点
- 否,则返回null
- 是,则判断first是否为树节点
- 是,则在红黑树中查找并返回节点,没有找到返回null
- 否,则在链表中遍历查找并返回节点,没有找到返回null
6、resize方法
final Node<K,V>[] resize() {
// 将table赋值给oldTab
Node<K,V>[] oldTab = table;
// oldCap表示原来table的容量(长度,如果table为null则为0)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 将threshold赋值给oldThr
int oldThr = threshold;
// newCap和newThr表示新容量和新临界值
int newCap, newThr = 0;
// 如果oldCap大于0
if (oldCap > 0) {
// 如果oldCap大于等于最大的容量2^30,则将threshold赋值为int的最大值,并返回oldTab
// 也就是说,此时不执行扩容操作,因为已经无法扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// newCap赋值为oldCap的两倍,如果newCap小于最大容量且oldCap大于默认初始化容量
// 则将newThr赋值为oldThr的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果oldCap为0,但是oldThr大于0,则将oldThr赋值给newCap
// 这种情况表明,底层数组还没有初始化
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 如果oldCap为0,oldThr也为0,则将newCap赋值为默认容量,newThr赋值为默认容量*默认负载因子
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// newThr为0,表明oldThr也为0,此时使用newCap来计算newThr(newCap * loadFactor)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将newThr赋值给threshold,此处就确定了newCap和newThr了
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 使用newCap创建对应大小的newTab数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将newTable赋值给table
table = newTab;
// oldTab中有数据,就将数据迁移到newTab中
if (oldTab != null) {
// 遍历数组中的节点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// e为当前正在遍历的节点,如果e不为null
if ((e = oldTab[j]) != null) {
// 将oldTab对应的位置置空,便于垃圾回收
oldTab[j] = null;
//如果e的next为null,表明e是一个单独的节点,则直接在newTab中对应索引位置插入该节点
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 {
// 之所以定义两个头尾节点,是因为链表中的元素在迁移过程中
// 要吗还在原来的下标,要么就在原来的下标+原来数组的长度的下标位置
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重新指向尾部节点
loTail = e;
}
// 高位为1,下标变为原下标+原数组长度
else {
if (hiTail == null)
// 第一个节点
hiHead = e;
else
// 尾部插入节点
hiTail.next = e;
// hiTail重新指向尾部节点
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;
}
TreeNode树节点类中的split()方法源码:
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// b指向当前对象(TreeNode类对象),由于HashMap只有resize方法遍历数组中的节点时用到了
// 所以这里的this一定是红黑树的根节点
TreeNode<K,V> b = this;
// 和上面一样高位和低位各自一个链表,头节点表示第一个节点,尾节点用来插入节点元素
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
// lc代表低位链表中节点的数量,hc代表高位链表中节点的数量
int lc = 0, hc = 0;
// e树节点的初始值为b,next也是一个树节点
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
// 高位为0,则将其插入到低位链表中
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
// 第一个插入的就是头节点
loHead = e;
else
// 尾部插入节点
loTail.next = e;
loTail = e;
++lc;
}
// 高位为1,则将其插入到高位链表中
else {
if ((e.prev = hiTail) == null)
// 第一个插入的就是头节点
hiHead = e;
else
// 尾部插入节点
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
// 如果链表中的元素数量小于去树化的临界值(6),则将其转换为Node对象的链表
tab[index] = loHead.untreeify(map);
else {
// 否则将其树化
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 高位链表相同的处理方式
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
现在我们来总结以下resize()方法的执行流程: 首先确定新的容量和临界值
- 首先确定原table的容量oldCap和临界值oldThr,根据这两个值来确定新的容量newCap和新的临界值newThr
- 如果oldCap大于0,说明table已经初始化,此时判断oldCap是否大于等于数组的最大长度
- 是,那么将临界值threshold赋值为Integer.MAX_VALUE,此时直接返回原来的数组。表明已经无法扩容了。
- 否则newCap变为oldCap的两倍,如果newCap小于数组最大长度,则newThr变为oldThr的两倍(不要担心oldCap为
的情况,此时如果超过临界值会继续上一步的过程将threshold设置为Integer.MAX_VALUE
- 如果oldCap等于0(不可能小于0),说明table还没有初始化,判断oldThr是否大于0
- 是,则说明调用了有参构造,指定了初始容量,此时oldThr的值就是要创建数组的大小,将其赋值给newCap
- 否,说明调用了默认构造,此时newCap为默认容量(16),newThr为默认容量 * 默认负载因子=12
- 如果此时newThr仍然为0,则计算其值(newCap * loadFactor),当然如果newCap大于等于数组最大长度,则newThr为Integer.MAX_VALUE
- 如果oldCap大于0,说明table已经初始化,此时判断oldCap是否大于等于数组的最大长度
创建数组并迁移元素:
- 创建大小为newCap的数组newTab,table指向newTab
- 遍历oldTab中的节点,下面是遍历的处理过程
- 如果节点是一个单节点,则通过hash & (newCap - 1)插入到对应的索引位置
- 红黑树和链表的处理有一部分思路相同,这里解释了为什么每次都扩容到原来的两倍
- 使用高低两个链表的头尾节点表示两个链表,判断hash & newCap是否为0
- 是则将其插入到低链表(尾插)
- 否则插入到高链表(尾插)
- 如果是链表,则直接将低位链表头节点插入到原来的索引位置,将高链表头节点插入到原索引+oldCap位置
- 如果是红黑树则需要特殊处理,如果链表长度小于等于去树化临界值(6),则将其转化为Node节点插入(红黑树节点会先强转,所以这里要转回来),否则将链表转化为红黑树然后插入
- 使用高低两个链表的头尾节点表示两个链表,判断hash & newCap是否为0
7、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;
// 对应索引位置的节点(p)不为空
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;
// 索引位置节点p的key和key相同,则node指向p
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// e为p的下一个节点且e不为空
else if ((e = p.next) != null) {
// 如果p是树节点,则在红黑树中查找该节点
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不为空,从红黑树或者链表中删除元素
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;
}
}
// 返回null表示没有找到要删除的key
return null;
}
remove方法没有什么好说的,要注意,在移除红黑树中的节点时可能会将红黑树转化为链表
画一个极端的情况下的例子:
看了这个图你知道为什么数量小于等于6就要变为链表了吧