其中绿色代表接口,橘色代表的是实现类由结构图可以看来HashSet实现了Set接口,LinkedHashSet为HashSet的子类。
添加一个元素的时候会先得到hash值,会转化为索引值 找到存储数据表table,看这个索引位置是否存放有元素 如果没有直接加入、如果有就调用equals方法比较,如果相同,就放弃添加,如果不同就添加到最后。
java8中,如果一个链表的元素个数>=treeify_threshold(默认为8),并且table的大小>=64就会进行树化(红黑树) 如果table数组长度没有超过64就扩展table数组。
add()方法
根据以下代码进行分析。
public static void main(String[] args) {
HashSet hs = new HashSet();
hs.add("java");
hs.add("c++");
hs.add("java");
System.out.println(hs)
}
HashSet hs = new HashSet();
HashSet底层是HashMap
//源码 public HashSet() {
map = new HashMap<>();
}
继续深入HashMap可以看到
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
其中有一个DEFAULT_LOAD_FACTOR为加载因子这个加载因子在HashMap中被定义
static final float DEFAULT_LOAD_FACTOR = 0.75f;
hs.add("java");
add方法调用了map的put方法
public boolean add(E e) {
//判断put函数的返回值是否为null
return map.put(e, PRESENT)==null;
}
可以看到有一个PRESENT的参数,这个PRESENT在HashSet类里面被定义为一个常量:
private static final Object PRESENT = new Object();
因此在HashSet中调用add()方法的时候是将add里面的参数存放在key中的,而value值则是一个常量用来占一个位置。(因此可以解释双列集合HashMap实现了单列集合HashSet)。
举个栗子:
例如添加了两个节点key值分别是"java"和"C++"
可以看到两个结点的value值一样都是java.lang.Object@1c6b6478。
继续追踪 map.put(e, PRESENT)函数
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
发现putVal函数里面有一个参数hash(key),进入到hash(key)里面
static final int hash(Object key) {
int h;
//将 hashCode 的高16位和 hashCode 进行异或(XOR)运算,得到最终的 hash 值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
!!!前方高能!!!
从hash(key)里面出来之后进入:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
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 {
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
treeifyBin(tab, hash);
break;
}
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;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
分析:
进入到putVal函数里面首先会判断table表是否为空,或者判断Node数组的长度是否为0,如果满足条件就会调用resize()函数为table数组去重新定义一个长度,目前此案例的table表为空。
因此会进入到if语句中
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
/*判断table是不是为为null如果为null返回表长为0,否则返回表长*/
int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap记录旧的容量
int oldThr = threshold;//OldThr记录旧的阈值
int newCap, newThr = 0;//分别记录新的容量和阈值
if (oldCap > 0) {
/*
如果满足旧表的长度大于0,就会判断
*/
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
//否则就会为table表设置一个新的容量DEFAULT_INITIAL_CAPACITY为16
newCap = DEFAULT_INITIAL_CAPACITY;
//新的阈值为加载因子*容量=12
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;
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;
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;
}
第一次进入到resize()函数里面table表为null,因此oldCap=0;
判断oldCap进入到
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
return newTab;
在else里面将newCap的值设置为DEFAULT_INITIAL_CAPACITY即16 ,newThr为 加载因子*容量=12。
再将newThr的值赋值给threshold(阈值),之后根据新的容量newThr去创建一个新的数组;将新的数组返回。
从resize()函数出来之后继续执行putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)函数
目前的table表情况:
之后执行:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
分析:根据(n - 1) & hash计算当前的Node应该放到table表的哪个位置,并且判断当前位置有没有存放其他的值。由于这是第一次去存放因此这个位置一定是空的,所以就在当前位置去创建一个新的结点。
++modCount;
//结点总数加一
//判断当前的总数size是否大于阈值,如果大于阈值则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
执行结果:最终返回到:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
返回结果为true因此第一个元素"java"添加成功!!
hs.add("c++");
同理追加进去:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
再进入到map.put(e, PRESENT)
函数
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
因为key值为"C++“不同于第一次添加的"java”,所以他们的hashCode()的值不一样因此调用hash(key)函数的值不一样,所以他们在HashMap中不会出现冲突,因此之后的执行过程与第一次添加"java"的过程一样。
执行结果:
hs.add("java");
同理追加进去:
public boolean add(E e) { return map.put(e, PRESENT)==null;
再进入到map.put(e, PRESENT)函数
public V put(K key, V value)
{ return putVal(hash(key), key, value, false, true);}
此时因为第二次添加的值为"java",因此会产生hash碰撞
进入到:
else {
Node<K,V> e; K k;
/*判断传进来的hash与当前的hash值是否相同并且判断key是否相同,!!!
key值的比较是通过== 或者 equals方法进行比较,具体的怎么才是key
值相同需要自己去重写equals方法来进行定义*/
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 {
//以链表的方式存储,遍历链表
for (int binCount = 0; ; ++binCount) {
//如果当前节点的下一个节点为NUll,那么就新创建一个节点挂到末尾。
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果结点个数大于或者等于8就调用treeifyBin(tab, hash)函数,可能将链表转化为红黑树。
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//或者在链表中找到了与这个结点key值一样的结点,那么就会添加结点失败退出
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
根据上面的代码可以看出进入到else语句中会遍历链表,采用的是for循环,这个for循环没有判断条件是个死循环,退出的条件是要么没有找到一个key值相同的就在链表的末尾去加上这个结点,要么是找到key值相同的就退出。
本案例是产生了冲突,并且当前链表中存在key值为"java"的结点,因此添加结点失败。
注意:
并不是链表结点个数大于或者等于8就直接被转化为红黑树,而是调用了 treeifyBin(Node<K,V>[] tab, int hash)函数,观察这个函数可以看到当table数组的长度小于MIN_TREEIFY_CAPACITY的时候不会转化为红黑树,而是将table数组进行扩容!
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
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);
}
}
图解:在链表上添加结点的过程
假设要添加一个节点"coco",计算其hash值与"apple"节点的hash相同即发生了冲突,但是链表上的key值都不相同的情况
执行 p.next()!=null; p = e;
执行 p.next()!=null; p = e;
执行 p.next()!=null; p = e;
执行 p.next()!=null;
执行p.next()==null ; p.next = newNode(hash, key, value, null);
假设要添加一个结点"potato",计算hash值与"apple"结点的hash值相同即发生了冲突,链表上已经有key值为"potato"的结点。
执行p.next()!=null; p = e;
链表中找到了与这个结点key值一样的结点,跳出循环,添加结点失败!
注意:
在链表转化为红黑树的时候需要满足的条件是链表的长度大于等于8并且table数组长度大于等于64,若链表长度大于8而table数组长度没超过64的时候需要对数组进行扩容。
举个栗子:
//此例子重写了hashCode使每个对象返回的hash值都一样以此来模拟产生了hash冲突
public class HashSetResourcesAna {
public static void main(String[] args) {
HashSet hs = new HashSet();
for(int i = 0;i<20;i++){
hs.add(new student("alice"+i));
}
}
public static class student{
public String name;
public Integer age;
public student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
student student = (student) o;
return Objects.equals(name, student.name) && Objects.equals(age, student.age);
}
@Override
public int hashCode() {
return 100;
}
}
}
目前,size的值为8但是因为table数组的长度小于64,所以不会转化为红黑树,但是会对数组进行扩容操作。
扩容后:
size的值为9但是table数组的长度依然小于64,所以继续对数组进行扩容操作。
扩容后:
size的值为10但是table数组的长度依然小于64,所以继续对数组进行扩容操作,此次扩容长度将达到64。
转化前:(HashMap$Node)
再添加一个同hash值的结点:
转化后:(HashMap$TreeNode)