HashSet扩容机制以及添加元素底层源码分析(超级详细的理解)
1.想看一段测试代码
package com.heyuanhang.conllention_;
import java.util.HashSet;
/**
* @Author 何远航
* @Date: 2021/4/18 11:33
* @Version 1.8
*/
public class HashSetSource {
public static void main(String[] args){
HashSet hashSet = new HashSet();
boolean java = hashSet.add("java");
}
}
(1)由上述代码可知现在添加的是一个元素,且使用的是HashSet的无参构造。
(2)执行无参构造时,看一下其源码:
//这是HashSet的无参构造器
public HashSet() {
//哦,原来这儿调用的是HashMap的无参构造方法
map = new HashMap<>();
}
注意:由上述源码可知,HashSet底层实际上就是一个HashMap;
(3)好了,现在要执行hashSet.add(“java”)方法了。进入add方法看一下:
public boolean add(E e) {
//调用map的put方法
return map.put(e, PRESENT)==null;
}
//map是HashMap类型的变量
private transient HashMap<E,Object> map;
//上面的PRESENT其实无实际意义
private static final Object PRESENT = new Object();
值得注意的是这个put方法再添加成功后会返回null,所以add整个表达式的值就为true,那添加失败会返回什么呢?(下面会提到,这儿只需要知道添加失败不会返回null。)
(4)现在就看一下HashMap的put方法的源码:
//其中形参key就是待存入的元素,而value就是指上面add方法出传来的PRESENT
public V put(K key, V value) {
//这儿会调用HashMap的putVal方法,且还会调用hash方法
return putVal(hash(key), key, value, false, true);
}
(5)执行hash方法,看hash方法的的源码:由下面的源码可知hash方法返回的是一个hash值,这个hash值要用于putVal方法中计算待存入元素要存入HashMap的table的位置,同时也可以知道待存入的元素的hash值并不等同于其hashCode值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(6)执行完hash方法后,会执行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;
//判断当前的table是否为空,这个table再new对象时就已经初始化为空了,第一次添加元素时
//肯定为空
if ((tab = table) == null || (n = tab.length) == 0)
//然后进行table的扩容,其实resize()方法会返回一个已经扩容好的table返回
//其实n在第一次添加元素时就是16
n = (tab = resize()).length;
//这人的i = (n - 1) & hash就是通过运算得到一个值作为待存入元素存到table中的位置
//hash就是通过hash方法获得的
//所以再加入元素时先判断一下要加入的这个位置是不是为null,如果是的话就直接存进去
//否则的话就进入else
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//1. 这下面的代码就是判断是不能加入还是挂载到要加入位置的尾部进行链表的形成
//2. 值得注意的是,当一个位置形成的链表的结点个数达到8并且table的长度大于64
//那么就会进行黑红树的转换
else {
//===================继续阅读之前你需要知道的======================
//1. 能进入到这个else语句那么就表示待添加元素key在table表中的i = (n - 1) & hash位置
//已经不为null了即就是这个位置已经有元素了,才会进入这个语句块进行判断。
//2. 在这儿会判断key与位置i = (n - 1) & hash已经存在的值相同不,如果相同就添加失败,否
//则就添加成功
//===============想必你应该知道了这个程序块大概是做些什么了,那就开始继续==========
//===============================================================================
//定义局部变量,作为辅助,(打过王者的都知道辅组也是很重要的,哈哈哈)
Node<K,V> e; K k;
//1.这个if语句的作用就是判断key即就是待添加的元素是不是与这个位置已经存在的元素是不是
//同的
//2.这个p在上面p = tab[i = (n - 1) & hash]这儿被赋值过,即就是指向位置i = (n - 1) &
//hash这个结点
//先判断hash值与key是否相同 ,不同时在判断key是否为空,且进行equals的判断
//这儿的equals绝对不能简单理解成时内容的判断,这个equals程序员可以添加自己的业务逻辑
//我认为 (k = p.key) == key当key是某个类对象得引用时,比较的是地址是否相同.
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);
//上面两个条件都不满足时,即就是不满足树结构的转换,以及不与位置[i = (n - 1) &
//hash这个结点元素相同
else {
//有一个死循环,退出的条件
//条件1:在要加入的位置i = (n - 1) & hash处所形成的链表没有一个结点与要加入的结点
//相同时,退出循环,此时就加在最末尾。添加成功
//条件2 :在要加入的位置i = (n - 1) & hash处所形成的链表有结点与要加入的结点相同
//此时退出循环,添加失败
for (int binCount = 0; ; ++binCount) {
//这是条件1
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//这是条件2
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);
//添加失败之后返回的oldValue
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在这里需要对上述扩容的方法resize()有必要看一下:
final Node<K,V>[] resize() {
//我的理解是
//1.这个方法其实做的就是当table为空时就扩容成一个容量16大小的table
//2.当未达到红黑树转换的条件并且再添加一个元素就超过临界值threshold时就进行2
//倍扩容
//这个threshold是临界值,其值是table当前的容量大小的0.75倍
//扩容完毕后就进行返回
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 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
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
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;
}
好了,到这里HashSet添加元素时的对底层源码的理解就写完了。希望对你有所帮助!
那么现在就可以看一下到练习题,
题目要求自定义一个Employee类,有私有属性name,和age,将员工添加到HashSet中,只要员工的名字和age相同则就添加失败,否则添加成功.
上代码
package com.heyuanhang.homework;
import java.util.HashSet;
import java.util.Objects;
/**
* @Author 何远航
* @Date: 2021/4/18 14:38
* @Version 1.8
*/
//这里来分析一下:想做出这个题必须要知道HashSet的底层是怎么比较的.
//1.首先想要使name和age相同的的员工,只能添加一个,那么就得使他们要添加到的位置必须相同
//2.那么怎么才能使他们要添加的位置相同呢?由底层源码可知要添加的位置是通过i = (n - 1) & hash
//来计算的,而hash又是通过hash方法来求得的,而table的长度在当前来说是相同的即就是n的值是相同的
//由此可知只要保证待添加的元素的hashCode值一样就行了.
//因此需要在Employee中重写hashCode方法
//3.在使得他们要添加的位置相同后,就得开始比较了,比较自然少不了重写toSring方法了
//4.分析到这儿,问题已经迎刃而解了,详细实现请看以下代码
public class HashSetDemo {
public static void main(String[] args){
HashSet hashSet = new HashSet();
hashSet.add(new Empolyee("heyuanhang",18));
hashSet.add(new Empolyee("heyuanhang",18));
hashSet.add(new Empolyee("heyuanhang",19));
System.out.println(hashSet);
}
}
class Empolyee{
private String name;
private Integer age;
public Empolyee(String name, Integer age) {
this.name = name;
this.age = age;
}
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 String toString() {
return "Empolyee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Empolyee empolyee = (Empolyee) o;
return Objects.equals(name, empolyee.name) &&
Objects.equals(age, empolyee.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
运行结果:
到此为止,HashSet扩容机制以及添加add元素底层源码分析就结束了.