之前研究过几次源码,通过看资料等各种方式,搬着源码一起看,最近对写代码这件事情有了新的认识,所以打算从另一个角度,去熟悉之前做的事儿;
此次打算写一个全面的源码解析,深入到源码中的构造函数–增删改查的方法;希望能在这个过程中,有比之前更深入的了解。
本文的源码是java11版本的HashMap
- 关于HashMap的简介:
- HashMap是一个关联数组、哈希表,它内部没有进行并发处理,所以是线程不安全的,允许key和value为null,无序集合;
- 它的底层结构是数组➕链表的形式,这个数组称之为哈希桶,每个桶节点中存储的是key的哈希值相同的键值对;JDK8对hashMap的结构进行了优化,当每个哈希桶存储的链表长度大于8的时候,就转换成红黑树存储,这样无论是插入还是查找,都提高了效率;
- 因为hashMap的底层是数组,就也涉及到了扩容的问题。当HashMap的容量到达threshould的阈值时,就会进行扩容。扩容后的容量是扩容为大于当前容量的2的N次方。这样根据key的哈希值寻找对应的哈希桶时,可以使用位运算代替取余。
- 根据key的哈希值确定元素在桶中的位置—这里的key的哈希值并不仅仅是key的hashCode的值,还会经过扰动函数的扰动,最后得到。因为key的hashCode是一个int值,其取值范围是40多亿,但是hashMap的默认长度Hi只有16,所以要进行扰动;
- 扰动函数的原理:为了解决hash碰撞,************** 待补充 ************
- 扩容操作的过程:每次的扩容,都会先新new一个Node数组作为哈希桶,然后将老得数组中的所有元素放入新的桶中,相当于重新给每一个元素进行put操作,所以扩容很消耗性能,hashMap的容量越大,则扩容消耗的性能就越大;
2. 源码解析
下面是一些常量说明:
// 默认容量必须是2的幂次
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量
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;
//---------fields
// 整个HashMap中的桶数组,初始化的时候容量设置为2的幂次,这里也容忍一些长度定义为0的操作;
transient Node<K,V>[] table;
// 主要用作 keyset()和values;
transient Set<Map.Entry<K,V>> entrySet;
//HashMap中存储的键值对的数量
transient int size;
//扩容阈值,当size>=threshold时,就会扩容
int threshold;
//HashMap的加载因子
final float loadFactor;
//这个字段主要记录的是hashMap结构变化的次数(eg:rehash),主要用于迭代的时候,HashMap是一个“快速失败”的集合类型,即:当迭代的时候,发现当前的map对象倍修改了,会立即抛异常ConcurrentModificationException,不再继续往下执行;
transient int modCount;
关于每个桶节点对应的链表中节点的源码Node解析[HashMap内部定义的单向链表]:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key的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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
//Node重写了toString方法,key=value;
public final String toString() { return key + "=" + value; }
// todo:这里不太懂 这个hashCode方法的重写,是用来干嘛的
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//Node重写了equals方法,主要是用来在put方法中,当key的哈希值相同时,再顺着链表去查找是否有和当前的<key,value> 值完全相同的,如果没有则在链表尾部追加当前元素;
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;
}
}
JDK7以及之前,每个桶节点都是只对应链表结构,JDK8开始,使用了红黑树,节点改为了treeNode;红黑树提高了查询效率;
//计算key的哈希值的方法,当key为null的时候,返回0,所以hashMap中允许有一个key为null的键值对;
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashMap的四个构造函数:
// 这个构造函数再研究一下
//判断自定义容量和负载因子,当自定义容量<0时和负载因子<0 则抛异常;如果自定义容量大于最大容量,则取最大容量作为初始化容量
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);
}
//此处负载因子为默认值
public HashMap(int initialCapacity) {}
//该构造的所有fileds均为默认值
public HashMap() {}
//这是将一个Map对象直接作为初始化的参数,这时候会调用putMapEntries()方法,对桶的长度等fileds进行初始化设置;
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//这里说一下最后一个构造函数,这里用到了一个很重要的方法:putVal();
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // 当table为null时,初始化容量
float ft = ((float)s / loadFactor) + 1.0F;
//size/负载因子+1的值和最大容量做比较,如果小于,则直接创建和map大小相同的即可,如果大于最大容量,则t=最大容量;
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
//如果t>扩容阈值,则阈值进行重新赋值;tableSizeFor方法,返回给定容量的两倍幂容量;
if (t > threshold) threshold = tableSizeFor(t);
} else if (s > threshold)
//当参数map的size大于最大容量,则进行resize todo: 这个方法后面讲;
resize();
//在初始化容量,或者rehash之后,将m
//todo: 这里没看懂。。。
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);
}
}
}
解析一下tableSizeFor()方法,这个方法主要是用来确定扩容之后的容量:
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
或者
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;
}
//说一下这中实现:阈值 = loadFactor * 容量;
//通过这个方法,如果初始值为19,那么扩容后 数组的大小是多少呢? n=18(10010),n>>>1=01001(无符号右移),和n位或运算之后是11011;
// 这时候n=11011, n>>>2=00110,和n位或运算之后=11111; 即32,2的5次方-1;
//即最后的扩容之后数组的大小,是比当前cap大的最小的2的幂次方;
下面上最重要的俩个方法 HashMap的put()方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* hashMap的hash方法
* 如果key为null,则将哈希值设置为0;否则 取key的hashCode() 并将key的hashCode的值无符号右移16位,再和key的hashCode做 按位异或运算;
* 所以hashMap的key可以有一个为null; 而hashTable的源码中,是判断,如果value==null,直接空指针异常;然后key.hashCode(),如果此时key为null,则直接抛空指针;
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
说明:"^" 是按位异或运算符 ,是二元运算符,要化为二进制才能进行计算,在两个操作数中,如果两个相应的位相同,则运算结果为0,否则1;
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent 如果是true,是否需要替换相同的value值; 如果为true,表示不替换已经存在的value
* @param evict 如果是false,则table正在被创建,表示数组是新增模式
* @return previous value, or null if none
*/
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. 如果tab还没有被初始化,先调用resize进行tab长度的确定;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 判断i = (n - 1) & hash]位置的桶节点是否为空: 如果为空的话,则new一个node;
// else 如果这个位置已经有了key-value对,则进行一些判断
// 这里特别说明一下 index的算法:index 是利用 哈希值 & 哈希桶的长度----代替位运算;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, 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)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则按照链表的形式,插入这对数据. java8是遍历链表,如果都不存在这个key,则在链表的尾部新增一个节点,
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果链表的长度>=红黑树的阈值-1,即当链表长度=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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
//如果onlyIfAbsent为false或者当前节点的value=null的话,则将传入的key-value赋值进来;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果当前hashMap的长度大于了阈值,则进行resize操作;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
当长度>=8的时候,链表是怎么转为红黑树的呢?
- [ 这里还是没怎么看懂,怎么从链表转为红黑树的?]
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 当map的桶数组为空或者容量 小于 链表转为红黑树的最小容量时 初始化数组
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//取下标 index = (n-1)& hash,
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方法():
- [key的hash是怎么实现的,hash(key)]
public V get(Object key) {
Node<K,V> e;
// 返回e.getval()
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;
// 当map桶不为空 且第一个节点也不为null的时候 ( 第一个节点的下标=(n - 1) & hash )
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// always check first node 首先先查询第一个节点的key是不是等于当前的key (等于的判断方式 1.节点的hash相等,节点的key和参数key相等 3.参数key和节点key的equal为true)
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果第一个节点不等于参数key ,则hasNext ,
// 第一个node如果是红黑树类型,则按照红黑树来进行查询
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 否则就是链表,do while{}, 当e的下一个节点不为null的时候,则去对比 节点的hash key的== 和 equal是否为true;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
/**
* 红黑树的查找方式
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
//使用红黑树的根节点去调用find,找到key对应的节点; root.(hash,key,null)
return ((parent != null) ? root() : this).find(h, k, null);
}
//getTreeNode调用的俩方法是 静态内部类 static final class TreeNode <K,V> extends LinkedHashMap.Entry<K,V> (){}中的俩个方法
final TreeNode<K,V> root() {
// 循环去找根节点 根节点的判断 r.parent = null;
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
//从红黑树的根节点开始去查询与key对应的节点;
- […………………………^_^] 这里的 p=this的this是当前节点么?
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 如果当前节点的hash值大于传进来的hash值,则传进来的key在当前节点的左边; p赋值为左边的节点
if ((ph = p.hash) > h) p = pl;
// 如果 当前节点的hash小于传进来的hash,则要查询的在当前节点的右边; p赋值为右边的节点
else if (ph < h) p = pr;
// 如果当前节点的key和key相等,且equal也为true 则当前节点就是要查询的节点;
else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p;
// 如果pl或者pr都为null,则赋值给相反方
else if (pl == null) p = pr;
else if (pr == null) p = pl;
else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 如果上面的条件都不满足: 手动将右节点赋值给p,调用find去迭代直至找到q;
else if ((q = pr.find(h, k, kc)) != null) return q;
// 如果上面的条件都不满足: 将左节点赋值给p,去左dowhile循环;
else p = pl;
} while (p != null);
return null;
}
get()方法逻辑总结
通过key的hash值和key去找node;
当map桶数组不为空的时候,且第一个桶节点也不null的时候,
首先一定先单独对比第一个first是不是当前要找的key; 如果是则直接返回;
如果不是,则判断当第一个节点 first.next 是否等于null;
如果等于null 则直接返回null 没找到key对应的值;
如果不等于null, 判断下一个节点是不是红黑树,如果是红黑树 则按照红黑树的方式查找; 否则,按照链表的方式查找;
链表的查找方式:
比较简单: 使用do while{}, 当e的下一个节点不为null的时候,则去对比 节点的hash key的== 和 equal是否为true;
红黑树的查找方式:
先通过root()方法找到当前红黑树的根节点; ( 循环去找根节点 根节点的判断 r.parent = null; )
再使用根节点 调用find方法,去查找;
find()方法的原理: 也是使用do…while{} 循环匹配查找
第一二步: 通过当前节点的hash值和传进来的参数hash值做对比,如果小于参数hash,要命要找的对象在当前节点p的右边,则设置 p=pr; 反之 设置为 p=pl;
第三步: 不大于不小于,那就是等于咯,这时候去判断当前的key的 == 和 equeal是不是都相等,如果相等的话,则返回当前节点;
第四步: 如果都不是,则去判断,如果p的做节点为null,则证明当前节点已经是左边的节点,没有比他更小的了,则p=pr; 反之p=pl;
第五步: 如果上面的四个步骤的if条件都不满足,则 考虑使用迭代 强制 对赋值pr pl 进行迭代查找, 这里的迭代很有意思,pr是作为一步骤,直接通过pr.find() 迭代进行查找,知道找到,返回节点值; pl呢则是以上所有的条件都不满足的时候,将p=pl; 这样pl进行do…while{}循环查找;
下面来看看 hashMap的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;
// 还是先判断桶数组 和 第一个节点是不是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;
// 首先还是一样,先看第一个节点是不是要找的节点;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 和get一样,分别判断 通过红黑树和链表的查找方式 先找到对应的节点node;
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,且node的value和参数的value也相同的时候, 分别以红黑树或者链表的形式,一处节点,并将modCount+1,size-1;
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);
//如果是当前节点,则将现在的table的index的位置赋值给 当前节点node的next指针,(即将node移除到链表了)
else if (node == p)
tab[index] = node.next;
else
//如果以上都不符合,就直接将node.next赋值给p的next,移除了node节点
p.next = node.next;
//修改计数加1,桶的size减一
++modCount;
--size;
//
afterNodeRemoval(node);
return node;
}
}
return null;
}
remove()方法总结:
首先 第一步和查找的逻辑一样,根据key的hash值和key去在红黑树或者链表中查出对应的节点 (依旧先查询第一个node节点是不是当前节点,然后判断是红黑树还是链表,再按照各自的方式查找);
第二步 移除。
1. 查找到的node的value也相等;判断如果是红黑树,则按照树的方式remove,负责按照链表remove;
2. 链表方式删除:如果是当前节点,则直接将index赋值给node.next节点; 否则:将node.next赋值给p.next
- [ 这里的将node.next赋值给p.next 不太懂]
三、 相关的一些问题
- 为什么HashMap的阀值为0.75,指的是什么?