Java中HashSet的add元素方法源代码个人总结
特点:
1.(细节!!!)当加入一个元素后,这时候修改元素内容后,其位置并不会改变!
2.如果这时候如果再增加一个与修改元素后相同的元素进来了,根据元素hash值,他的位置在其他位置!
3.如果增加一个与之前未修改的第一个元素一模一样的元素进来,根据hash值他的位置跟第一个元素位置一样,但是内容不一样,新加入的元素会挂载在修改了的元素后面!
- HashSet是无序的;(放入元素顺序和打印元素顺序不一致!是根据HashCode值来确定元素顺序的,而不是根据放入元素顺序)
- HashSet的底层是HashMap,HashMap的存储机制是节点数组+单向链表。
- HashSet不存储相同元素,最多只包含一个null,且没有索引,遍历集合只能用增强for循环或者Iterator迭代器(idea中用itit)。
- 方法属性前无synchronized,所以多线程是不安全的。
目录:
4.第四次往HashSet里添加hash值相等,对象地址不同,且无重写equals方法的自写类,从而进行节点链表挂载
1.第一次往HashSet里添加元素
- 创建一个HashSet对象
//创建一个HashSet对象
HashSet col = new HashSet();
- 第一次向里面增加元素
//向里面增加元素
col.add("java");
- 打断点,进行debug
- debug调试,Step Into(F7),进入源代码, 可见HashSet的底层是调用HashMap.put添加元素的方法
- 进入HashMap.put添加元素的方法中,这时候的 key就是我们往HashSet中增加的元素 java,其中hash(key)返回元素的Hash值,然后调用putVal(主要方法);
- 分析putVal(主要方法);
final V putVal(int hash , K key , V value , boolean onlyIfAbsent ,boolean evict ) {
//1.这里定义一个节点数组tab,节点p,整数n,整数i
Node<K,V>[] tab; Node<K,V> p; int n, i;
//2.HashMap实体类的属性Node<K,V>[] table初始值为Null
if ((tab = table) == null || (n = tab.length) == 0)
//3.resize()改变数组大小,这里进入resize()方法,
//(将resize()返回来的节点数组指向tab,并将长度16给n)
n = (tab = resize()).length;
//4.根据i = (n - 1) & hash,通过HashSet元素的hash值计算出元素在节点数组中存储的位置,并将这个位置的元素赋值给p,然后判断这个元素是否是空
if ((p = tab[i = (n - 1) & hash]) == null)
//5.因为这个元素是空,就将要存储在HashSet中的元素放入这个HashMap的节点数组中
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;
}
}
//6.增加修改次数一次
++modCount;
//如果现在存储完数据的HashMap的元素个数+1大于边界值
if (++size > threshold)
//就再扩容
resize();//就再扩容
//7.表示这个hashmap的元素插入成功
afterNodeInsertion(evict);
//8.然后返回空
return null;
}
//3.1进入了resize方法
final Node<K,V>[] resize() {
//3.1.1.HashMap的table初始值为null,所以这里创建的局部节点数组oldTab也为null
Node<K,V>[] oldTab = table;
//3.1.2.oldCap为这里创建的老容量为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//3.1.3.初始容量边界限制值threshold为0,所以这里创建的局部变量oldThr也为0
int oldThr = threshold;
//3.1.4.这里创建两个两个新变量newCap和newThr都为0
int newCap, newThr = 0;
//也就是说HashMap的初始数组table的Cap容量如果大于0
if (oldCap > 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; // double threshold
}
//也就是说HashMap的初始数组table的Cap边界容量如果大于0
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//3.1.5.也就是说HashMap的初始数组容量如果为0
else { // zero initial threshold signifies using defaults
//3.1.6.DEFAULT_INITIAL_CAPACITY(默认初始容量为16),然后赋值给newCap
newCap = DEFAULT_INITIAL_CAPACITY;
//3.1.7.DEFAULT_LOAD_FACTOR为(默认边界因素0.75)*DEFAULT_INITIAL_CAPACITY(默认初始容量为16),即这里边界值newThr为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);
}
//3.1.8.将新边界值传给HashMap初始容量边界限制值属性
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//3.1.9.根据容量16创建一个新节点数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//3.1.10将新节点数组地址传给传给HashMap的table数组属性
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;
}
}
}
}
}
//3.1.11返回新Table节点数组对象()地址
return newTab;
}
2.第二次往HashSet里添加元素
(因为第一次加入元素时数组已经扩容至16个大小,所以就直接根据元素的哈希值放入数组对应的位置)
- 第二次增加元素,并进行debug调试
- 还是继续调用HashMap的put方法
- 继续调用HashMap的putVal方法
- 分析putVal方法
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.将节点数组table指向tab,并判断数组对象地址是否为空或数组长度是否为空,
//这里由于第一次已经扩容,所以不满足条件
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2.再根据要增加元素的哈希值计算出索引值,然后将数组中的索引对应的节点数组元素赋值给p,
//并判断这个节点数组元素里面是否为null,这里对应的tab[13]节点元素为null,满足条件
if ((p = tab[i = (n - 1) & hash]) == null)
//3.创建一个以增加元素的数据包装成一个节点,并且放入节点数组中
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;
}
}
//4.修改次数加1
++modCount;
//5.判断是否超出边界值
if (++size > threshold)
resize();
//6.增加成功
afterNodeInsertion(evict);
//7.返回null
return null;
}
3.第三次往HashSet里添加字符串相同的元素
- 增加与第一个元素相同的元素,并调试
- 还是继续调用HashMap的put方法
- 继续调用HashMap的putVal方法
- 分析putVal方法
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.将节点数组table指向tab,并判断数组对象地址是否为空或数组长度是否为空,
//这里由于第一次已经扩容,所以不满足条件
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//这里根据哈希值算得与第一个增加的元素一样的索引,并将索引对应的节点赋值给p,
//这里不满足条件,执行else
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//2.执行这里
else {
//创建一个节点e
Node<K,V> e; K k;
//一.将数组中里面已经有的节点的哈希值和key对象与将要增加的节点的哈希值和key对象进行对比,判断是否是一个对象,并将里面已经有的节点的key对象指向k
//二.判断对象是不是为null,且通过key对象的equals方法(重点!!!)判断key是否相同,
//主要看节点对象的equals方法怎么比较对象的,即可以比较对象的地址,也可以比较对象的内容,可以比较对象的各种东西,主要看这个对象的equals方法时怎么写的!!!
//无论是静态还是动态创建对象,像字符串的equals方法默认比较的就是字符串的内容,
//equals(Object anObject) 将此字符串与指定对象进行比较。
//自己写的类,如果没重写equals方法,默认就是比较类的实例对象的地址
//下面有截图案例!!!
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
//3.如果满足条件,将新添加的节点不用,并将节点数组里面就有的原节点p指向给节点e
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;
}
}
//4.这里就是判断value是否唯一...返回原来就有的值返回给原来有的数组节点就行
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;
}
- 截图案例
4.第四次往HashSet里添加hash值相等,对象地址不同,且无重写equals方法的自写类,从而进行节点链表挂载
原理:hash值相同,equals方法比较出来相同就去掉,如果比较出来不同就挂载!
- 书写代码
public class Demo {
public static void main(String[] args) {
HashSet h1 = new HashSet();
//向里面循环添加A的实例化对象,进行挂载
for (int i = 0; i <7 ; i++) {
h1.add(new A(i));
}
}
}
class A{
//属性
int i;
//构造方法
public A(int i) {
this.i = i;
}
//方法
//重写hashCode()方法,所有A的实例化对象的Hash值都是100,
//因为没有重写equals方法,所以Hash值相等,且对象地址不同,就进行节点链表挂载
@Override
public int hashCode() {
return 100;
}
@Override
public String toString() {
return "A{" +
"i=" + i +
'}';
}
}
- 进行debug调试,还是继续调用HashMap的put方法
- 调用putVal方法
- 分析putVal方法
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.第一步添加第一个元素,会进行扩容。数组扩容至16个大小的table数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2.table节点数组扩容后,将第一个对象包装成节点放入节点数组中,然后第一个对象放入完成
//3.第二步添加第二个元素,根据第二个对象的hash值,算出与第一个对象一样的索引位置,
//所以不满足这个条件,但是p = tab[i = (n - 1) & hash]),原来存在的节点赋值给p点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//4.第二个元素进入到这里
Node<K,V> e; K k;
// hash值相同,p.key就是原节点里面的对象,但是对象地址不同,所以对象不同,
//且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) {
//5.由于第一个原节点的的后面为空,
//所以与第一个hash值相等的不同的第二个对象挂载在第一个原节点的后面,然后退出循环
//6.第三个元素走到这,先查看第一个节点后面是否为空,
//因为第一个节点后面挂载了第二个节点,就执行下面的if语句,
//但是第二个节点这里赋值给了节点e
//8.第二个节点后面的元素赋值给节点e,但是第二个节点后面为null,
//所以这时候第三个元素包装成节点挂载在第二个节点后面,其它的都如此
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;
//7.因为第三个节点不等于第二个节点,所以这里将第二个节点赋值给p,进行继续死循环
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;
}
其它原理:
1.当节点数组个数加链表节点个数的和是全部HashSet节点个数(size),
2.节点个数第一次初始化(size)是16个,但是有个临界缓冲值就是当存入的节点个数大于等于size * 加载因子(0.75),这里是大于等于12个节点时,就扩容,以原来size的2倍进行扩容,这里就是扩容至32个。
3. 当节点数组中的一条链表的长度等于8个并且节点数组大小大于等于64个节点时,这条链表转化成红黑树进行存储!