HashSet的底层添加机制
HashSet不允许列表中存在相同的值并且里面的数据元素无序
结论
-
HashSet的底层是HashMap
-
添加一个元素时,会先得到Hash值(HashCode方法) ,对Hash值进行会转成->索引值,也就是要存放在HashTable中的位置号。
-
找到存储数据表table,看这个索引位置是否已经存放的有数据‘
-
若是没有数据,则直接在这个索引上加入新数据
-
若是有数据,则调用equals方法比较,如果相同,就放弃添加;如果不相同则添加到链表最后。
-
在Java8中,如果一条链表的元素个数到达TREELFY_THRESHOLD(该值默认为8),并且
table的大小默认>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
-
如果链表的元素个数到达了TREELFY_THRESHOLD但是table的大小没有到达MIN_TREELFY_CAPACITY,则他会把table进行扩容
简单说就是
- 先获取元素的hash值(hashCode方法获取)
- 对hash值进行运算,得出一个索引值即为要存放在hash表中的位置号
- 如果该位置上没有其他元素,则直接存放
- 如果该位置上已经有其他元素,则需要进行equals判断,如果相同则不再添加。如果不相同,则以链表的形式添加。
- 关于equals方法,每一个类都会有自己的equals方法,比较String中重写equals方法来比较内容是否相同
底层源码
接下来将通过debug操作深入底层
###第一次添加
HashSet set = new HashSet;
set.add("java");
set.add("php");
set.add("java");//这个是故意的,事实上不会添加。
System.out.println("set="+set);
-
在
HashSet set = new HashSet;
下断点进行debug操作,此时它会调用HashSet的构造器HashSet()Code位置:HashSet.java
public HashSet(){
map = new HashMap<>();
}
此时可以发现HashSet的底层走的是HashMap。
-
接下来运行到达
set.add("java")
,此时它会调用并执行add
方法Code位置: HashSet.java
public boolean add(E e){ //e: "java"
return map.put(e, PRESENT) == null; //map: "{}" e:"java"
}
这里可以看见它是通过调用put
方法,并判断其是否为null
来确定是否添加这个数据。 断点进行下一步将会到达HashMap内的put方法。
Code位置:HashMap.java
public V put(K key, V value) {// key: "java" value:Object@550
return putVal(hash(key), key, value, false, true);
//key: "java" value: Object@550
}
PS:value
的值来自于PRESENT
,PRESENT
是定义在HashSet.java的属性,定义语句如下:
private static final Object PRESENT = new Object();
可以知道这是一个final类型的静态对象,但是它实际上没什么意义,它在这里主要起到一个占位的目的。就是为了让HashSet使用到HashMap,它的value统一放的都是一个Object。总之key的值变化,但value的值传进来的始终是PRESENT
可以看到put
方法返回putVal
方法返回的值,putVal
方法会执行hash(key),得到key对应的hash值。
接下来分析putVal
方法中接收的值都是些什么东西
-
hash(key)
这个方法是计算hash值的,最终返回一个key的hash值,hash值可以确定数据在数据表中的位置
Code位置:HashMap.java
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode) ^ (h >>> 16); // key: "java" /** 1. 三目运算符 如果key不为null,则按照(h = key.hashCode) ^ (h >>> 16)这个算法得到key的hash值 2. >>>是无符号右移的意思,这里无符号右移了16位 3. ^是按位亦或的意思 4. 这个右移 16 位再异或的操作是为了让 hashCode 的高位参与到哈希值的计算中,从而增加哈希值的分 布性,减少哈希冲突的概率 */ }
注意,这个hash值并不完全等价key的HashCode,因为它在此基础上进行了计算。
-
再进行下一步,此时会运行到
putVal
的底层源码
Code位于 :HashMap.java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict){
//hash: 3254803 key: "java" value: Object@550 onlyIfAbsent: false evict: true
Node<K,V>[] tab; Node<K,V> p; int n, i; //1. 定义了用于辅助的变量
if((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2.无论如何,执行了resize后,table事实上已经变成了大小为16的node数组了
// 这块语句表示:如果当前table是null或者大小== 0 ,就是第一次扩容,直接扩容16个
if((p = tab[i = (n - 1) & hash]) == null)
//3.计算key(key:"java")对应的hash值来决定key应该在tab表中的哪一个索引位置去存放,并且把这个位置的对象赋给辅助变量p。
//判断p是否为空,如果p为空则表示该索引位置没有存放过元素,则执行newNode在tab[i]43创建一个node
tab[i] = newNode(hash, key, value, null);
// newNode接受4个变量,hash是key的hash值、key:"java"、value=PRESENT,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 >= TREEIF_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) {
V oldValue = e.value;
if(!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//记录修改次数
if(++size > threshold)
//size初始化为0,执行完必后增加一次,这里是判断当前数组元素的数量是否超过阈值
//超过了就执行resize方法扩容。
resize();
afterNodeInsertion(evict);
//这个方法是HashMap留给子类例如LinkedHashMap去实现再做操作的,对于HashMap来说这个方法是空方法,可忽略
//子类可以去重写这个方法来实现例如有序链表之类的操作
return null;
//返回空代表成功
}
//最后java添加在索引3的位置,这是HashSet的主要原因之一
上面涉及到到的方法
-
resize()
Code位置:HashMap.java
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;//1. 这个table在HashMap定义了,初始化为null int oldCap = (oldTab == null) ? 0 : oldTab.length;//2. 判断为null,给oldCap赋值为0 int oldThr = threshold;//threshold是HashMap的属性,int类型初始化为0 int newCap, newThr = 0; if (oldCap > 0) { //oldCap = 0,进入else哪里 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold // 双阈值 } else if (oldThr > 0) // initial capacity was placed in threshold //初始容量处于阈值 newCap = oldThr; else { // zero initial threshold signifies using defaults //零初始阈值表示使用默认值 newCap = DEFAULT_INITIAL_CAPACITY; //新的capacity大小,就是数组开辟了多大的空间 // DEFAULT_INITIAL_CAPACITY是定义在HashMap的常量,初始化为1 << 4;(即1*2*2*2*2 = 16) //ps:位左移相当于*2,移动几位乘几次 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //新的threshold,新计算的临界值,即0.75 * 16 = 12,也就是说当capacity内的元素到达12个就 进行扩容操作的准备。这个是一个加载因子的操作,一个缓冲层,防止大量数据使程序崩溃。 //DEFAULT_LOAD_FACTOR,常量,0.75f } 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; //创建一个新的Node数组,容量即为刚刚开辟的空间,然后将这个新node数组赋给table。 //到了这里实际上已经将node数组扩容好了,到这里正常的流程基本结束,直接返回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; },
-
afterNodeInsertion(evict)
void afterNodeInsertion(boolean evict) { } //空方法,这个是给子类去重写实现的比如LinkedHashMap
重写后:
Code位置:LinkedHashMap.java
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
第二次添加
第二次添加和第一次添加存在不同,但都是依靠HashMap中的putVal方法进行添加,我们现在要添加"php"
所以此时key: "php"
,它的hash值也不一样,putVal会根据hash值进行与运算来给"php"进行分配索引
/**
int hash = hash(key); key: "php"; value: PRESENT
*/
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)
//因为table表已经存在,所以这里判断为false
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//通过hash的与运算得到了key的位置,判断为空后创建一个新的节点
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;
}
//最后"php"被添加到了索引9
第三此添加:添加重复数据,数据不添加
hashSet不允许同一个数据存在
/**
key: "java" value: PRESENT
*/
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;
//同样因为表存在它判断为false
if ((p = tab[i = (n - 1) & hash]) == null)
//注意,因为在第一次添加时添加了“java”,再次添加"java"时一定会判断计算出来的索引不为空
//因为这个java的hash值和第一个"java"的hash值相同
//判断为假,进入else
tab[i] = newNode(hash, key, value, null);
else {
// 接下来它分成了三种情况:
Node<K,V> e; K k;// 这里是创建了两个辅助用的局部变量e和k
if (p.hash == hash &&
//在满足 准备加入key和p指向的Node节点的key是同一个对象(就是hash相同)的前提下
((k = p.key) == key|| (key != null && key.equals(k))))
//满足以下条件
// (1)准备加入的key 和 p 指向的Node节点的key是同一个对象
// (2) key不为空且p指向的Node节点的equals()方法和准备加入的key比较后相同
e = p;
//将p赋给e
else if (p instanceof TreeNode)
//如果第一个if为false 判断是否是红黑树
//如果是,就调用putTreeVal,进行添加
//putTreeVal内带有大量与红黑树有关的复杂算法,暂且不追
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//以上两个情况都不满足,这个时候就会启用一个循环比较的机制
//如果table对应的索引位置,已经是一个链表,就使用for循环比较
//为什么是用for循环进行比较?因为索引内元素是一个链表,要进行链表的遍历一个个比较
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//假如遍历后没有发现链表内有相同的元素,并且下一个节点为null
//则创建一个新节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st(索引从-1开始)
//TREEIFY_THRESHOLD初始化为8
treeifyBin(tab, hash);
//在转成红黑树时还进行一个判断,这个判断在treeifyBin的方法内
//如果链表的节点超过9(即循环次数超过7),就将其红黑树化
/
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//如果依次和该链表的每一个元素比较后有相同的,退出循环
p = e;//注意:p在每一次遍历后都会改变指向的节点
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回PRESENT,反正不返回空,就是失败。
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
treeifyBin(tab, hase)
Code位置: HashMap.java
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();
//MIN_TREEIFY_CAPACITY的值初始化为64
//这里会进行判断,如果tab的长度小于64,则进行扩容操作,只有这个条件不成立时才进行红黑树化
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);
}
}
底层后总结
扩容和红黑树是不同的机制
- HashSet的底层是hashMap,第一次添加时,table数组扩容到16,临界值(threshold) = 数组大小*加载因子(loadFactor是0.75)得到的,一开始为12,即threshold = 16 * 0.75。
- 如果table的数组使用到了临界值12就会扩容到16 * 2 = 32 ,此时临界值threshold就是32 * 0.75 = 24,剩下由此类推。
- 在Java8中,如果一条链表的元素个数到达了TREELFY_THRESHOLD默认是8,并且table大小 >=MIN_TREEIFY_CAPACITY(默认是64)就会进行树化(红黑树),否则仍然采用数组机制扩容。
哈希计算索引机制
由于链表添加的索引是通过计算hash值得到的,且在同一个索引下,如果数据不存在相同则直接挂在到该索引内的链表的后面,那么就可以这样干
-
当我们需要在链表上的一个索引挂载多个元素时,我们可以重写hashCode方法
public class Main{ public static void main(String[] args){ HashSet hashSet = new HashSet(); for(int i = 0;i <= 12;i++){ hashSet.add(new A(i)); } System.out.println("hashSet= " + hashSet); } } class A { private int n; public A(int n){ this.n = n; } @ Overridden public int hashCode() { return 100; } //这样A对象所有的hashCode都是一样的 }