HashSet/HashMap底层源码解析
注意:这里仅仅只针对JDK8进行源码分析
HashSet new的过程实际上是创建HashMap过程,首先看一下测试代码
测试代码
@Test
@SuppressWarnings("all")
public void hashSetTest(){
Set hashSet=new HashSet();
hashSet.add("李白");
hashSet.add("杜甫");
hashSet.add("李白");
}
源码一:new HashSet()调用构造器的源码
private transient HashMap<E,Object> map;
public HashSet() {
map = new HashMap<>();
}
源码二:new HashMap()是对loadFactor属性的初始化过程
final float loadFactor;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//这里的DEFAULT_LOAD_FACTOR是一个扩容机制的扩容因子,0.75符合泊松分布
//泊松分布的选择,是hash冲突和空间利用率的折中方案
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
源码三:new完成以后,调用add方法
private static final Object PRESENT = new Object();
//这里应该就给很多人一个解答了
/*
*为什么HashMap存储的Key-Value,而HashSet如果想要底层实现HashMap,不得也有一个Value值才行吗?
*答:这里value值始终固定为Object PRSENT,且从对象创建开始就已经生成不改变
*即:Value设置了一个固定地址值
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//HashMap中的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
源码四:调用了put方法后,我们就发现还是在套娃,调用了putVal方法
这里我们先不解析putVal方法,先看五个参数
//hash(key):这里是对字符串做hash处理,生成一个hash码
static final int hash(Object key) {
int h;
//这里需要说明一下:如果key为空,则hash就是0,如果不为空,则需要使用key生成一个hash码后异或(^)上(hashCode的值无符号右移16的值)
/*
* 为什么要使用hashCode无符号右移16
* 取出hashCode码的高16位
* 首先我们知道int是4字节--32位bit
* 其次,这个无符号右移实际上是为了后续转换为数组索引做准备的
* 这个点会在putVal()方法中进一步解释
*/
//为什么要无符号右移以后还要异或,为什么使用^而不是&|呢
//是为了均衡分布,避免hash冲突--看图1
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//key--实际就是我们需要添加的值--"李白"
//value--PRESENT
//false--这个值是否不存在,如果存在,则不改变已存在的值
//true--这个就不重要了,因为使用它的方法并未实现,而是一个空方法
afterNodeInsertion(evict);
void afterNodeInsertion(boolean evict) { }
图1:这里我们可以看出,&、|这些逻辑运算结果都是偏向于true或false的,只有^才能让hash值更加随机散列
重点来了:
源码五:调用putVal()方法
transient Node<K,V>[] table;
static final int TREEIFY_THRESHOLD = 8;
transient int size;
int threshold;
//这个先不要看,直接从putVal开始看,从putVal中看这些东西是做什么的
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;
}
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;
}
}
//这个先不要看,直接从putVal开始看,从putVal中看这些东西是做什么的
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
//首先这个参数已经解释过了
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//这里的Node是一个静态内部类--作用是创建一个Node节点--也就是上面的static Class Node类
Node<K,V>[] tab; Node<K,V> p; int n, i;
//首先,如果是第一次创建,毫无疑问table是null,如果是第二次add,则这个table就不是null了--因为HashSet只new了一次
//所以这个if语句判断的是,如果这个Node数组是null,并且table表的长度也是0,那就开始初始化呗--后面再解析resize()方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果table表i这个索引处没有值,那就存放一个Node值呗
/*
* 这里就需要解释一下,为什么是无符号右移16位了
* 这里n是table表的长度,而我们知道table表的长度一般很少在高16位,导致了一个问题
* table表的长度永远是和低16位的hash值比较
* 那么如何能让低16位的值更加随机,就只能采用高16位的值先和自身来一次异或,再与n进行与运算,这样取得的索引值就会更随机
* 随机的目的是:避免一个节点上挂载的链表过长,而数组都还没存满--导致头重脚轻
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果i索引处有值
Node<K,V> e; K k;
//1.那就把这个"李白"的hash值和p=table[i=(n-1)&hash]处的hash值进行比较
//相等就继续判断,不相等就拉倒
//2.接着就会继续判断这个"李白"和p处的字符串是不是同一个地址(当然如果不是引用类型这里就需要其他理解--自行理解)
//3.判断这个字符串的内容是否与p处的字符串的内容一致(调用了equals方法)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果都满足--则将p的引用给e--都满足就覆盖一下呗
e = p;
//否则如果上述条件不满足,看看是否p节点已经被转换成了红黑树--没有转换就不执行呗,转换了就直接加在树的节点上
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//上述条件也不满足,也没有转换成红黑树
for (int binCount = 0; ; ++binCount) //死循环搞一手
//到这里,我们知道p处有值,且不与"李白"这个值相等
//但是p是一个Node链表,所以我们仍然需要遍历p这个链表,遍历完我们才能知道p链表是否符合存放"李白"这个值的资格
/*
* 1.如果p的下一个节点是空的--代表这个p就一个节点且有值,那就直接添加呗
* 2.如果循环的参数大于等于链表的最大长度-1,则变成红黑树--这也就是为什么会有判断p是否等于TreeNode的原因
* 3.链表的长度到底是等于8时转换,还是大于8时转换,还是等于7时就转换呢
* 3.1当binCount=0时先++,也就是当binCount=1时就进来执行添加操作--而这时p已经有了一个节点,说明binCount可以代表链表的索引值(添加的是第二个,binCount=1)
* 3.2这里的逻辑是添加完成后再判断,所以是添加完第8个以后,此时binCount=7,TREEIFY_THRESHOLD-1=7比较结果相等哇,转换一下
*/
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//4.但是如果p的下一个节点不是空的,那就比较一下呗,这里就不重复描述了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//e一直指向p的后继节点--如果后继节点不为空,则继续循环
p = e;
}
}
/*
* e的值分析:
* 如果链表没有满8,则e为空
* 如果链表满8,转换成红黑树,则e为空,因为putTreeVal()方法返回值为null
* 如果"李白"已经存在,则e=p e不为空
* 那么:
* 1.如果"李白"已经存在,则按照此逻辑,返回Object()的地址值
* 2.为什么要已经存在就要返回一个object地址呢,因为add方法中return map.put(e, PRESENT)==null,如果是null则添加成功,否则添加失败
*/
if (e != null) { // existing mapping for key
V oldValue = e.value;//Object()--PERSENT
if (!onlyIfAbsent || oldValue == null)//true||fasle
e.value = value;//Object()--PERSENT
afterNodeAccess(e);
return oldValue;
}
}
//这里就是一个扩容机制:
/*
* 如果添加完数据后,modCount就会++
* size也会++
* threshold在第一次扩容的时候就已经赋值了,等会再讲
* threshold=(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)=16*0.75
* 所以,如果size>12就会开始扩容
*/
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
后续再详细分析一下红黑树吧,害