其中绿色代表接口,橘色代表的是实现类由结构图可以看来HashSet实现了Set接口,LinkedHashSet为HashSet的子类。
添加一个元素的时候会先得到hash值,会转化为索引值 找到存储数据表table,看这个索引位置是否存放有元素 如果没有直接加入、如果有就调用equals方法比较,如果相同,就放弃添加,如果不同就添加到最后。
java8中,如果一个链表的元素个数>=treeify_threshold(默认为8),并且table的大小>=64就会进行树化(红黑树) 如果table数组长度没有超过64就扩展table数组。
根据以下代码进行分析。
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)
继续追踪 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数组去重新定义一个长度。
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)函数
之后执行:
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值一样的结点,跳出循环,添加结点失败!