HashSet中add方法源码详解
HashSet中add方法源码详解
HashSet简介
HashSet是Java容器框架中的一个重点,它是Set的实现类,继承了AbstractSet, HashSet是用来存放单值不重复的容器,其特点是对于存入的值,不保证其存入的顺序,以及不能重复,HashSet允许键值为null,但只能有一个null键值。下面我们从创建一个空的HashSet开始,然后向里面添加元素,一步步的分析源码来认识HashSet是怎样实现的。
源码分析
在进行源码分析之前,我们先写一段代码,然后根据这段代码一步步的深入源码,进行分析。代码很简单,如下:
Set<String> set = new HashSet<>();
set.add("110");
set.add("110");
set.add(new String("110"));
System.out.println(set.size())
1.构造方法
先看代码第一行,Set<String> set = new HashSet();
,从字面意义上来理解,有以下几点:
- 这句代码调用了HashSet无参的构造方法,创建了一个新的HashSet对象
- 将对象的引用赋值给了它实现的接口Set
- 对于Set中的元素传入泛型参数String,这样在编译期间,这个Set里面能够存放的数据类型就只能是String类型(String类是final的,不能被继承)。
HashSet无参构造方法
下面来看HashSet的构造方法源码:
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
代码第一行是声明了一个HashMap类型的成员变量map
,泛型内指定的类型为<E, Object>。这里可以先抛出结论,HashSet的底层实现是使用HashMap来实现的,HashSet利用了HashMap中key值不重复的特点,用来存储自己的元素,而map中对应的的value就是第二行的静态常量 PRESENT,是一个Object对象。稍后我们可以验证这一点。
根据以上代码,可以看到,HashSet的无参构造方法被调用时,只是调用HashMap的无参构造方法初始化了成员变量map
,下面我们看HashMap的无参构造方法。
HashMap无参构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
可以看出,HashMap的无参构造方法也很简单,只是设置了一下成员变量加载因子loadFactor
的值,值是一个静态常量0.75f,并未对其容量进行设置。然后我们看HashMap中用来存储数据的成员变量是怎么声明的:
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
文档注释中已经说明,这个table
在第一次使用的时候才会初始化,而且一定会会在第一次使用的时候调整大小。table
的长度一定是2的幂。我们再看一下size属性的声明:
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
因此我们可以得出一个结论:使用无参方法构造创建HashSet对象以及HashMap对象时,它的初始容量是0(而不是很多人说的默认容量就是16),只有在第一次使用的时候对它进行初始化。
2.向HashSet中添加元素
下面看代码第二行:
set.add("110");
调用set对象的add方法,如果我们直接点add进入源码,会发现我们进入的是Set接口的源码,这是因为Java多态的特性,在编译期看到的是父类的方法,但是在运行期会识别运行的类型,调用方法时,如果发现子类重写了父类的方法,就会直接调用子类的方法。所以我们直接看HashSet的add方法源码就行,代码如下:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
这里就已经验证了我们上面说的,利用map中key值不重复的特点存储元素,至于map中的value,存的就是一个静态常量,值是一个Object对象。
继续看map的put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
发现put方法又调用了putVal方法,在查看putVal方法之前,先来分析一波:
- HashSet的add方法要添加的元素e,传入了HashMap的put方法的key值,而value传入的是一个静态常量
- putVal()传入的第一个参数调用了hash()方法,传入的参数是key值(也就是最初HashSet中add的元素),看名字就知道是计算hash先看这个方法是怎么处理的。代码如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到,这是一个静态方法,接收的参数类型是一个Object,传入的key值被向上转型为Object。
下面看hash的计算,如果传入的对象是一个null值,直接返回0;否则会先计算key对象的hashCode(),这里运用了Java多态,如果key值对象重写了HashCode方法,就会调用重写的HashCode()方法,否则就会调用Object中的HashCode()方法。这里会调用String的hashCode方法。之后对得到的hashCode先进行无符号右移运算,再与之前的hashCode进行异或运算,最后计算的值返回。
结论:一个对象的hashCode()方法计算的值相同,hash()方法计算时的值也一定相同。
现在让我们回到putVal方法。
HashMap中的putVal()
此处建议点开源码跟着看。
我们先看putVal方法的定义:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)
返回值类型是一个泛型V也就是value的类型,是final的不能被重写。下面就参数列表中的值与put方法中传入的参数,putVal(hash(key), key, value, false, true);
进行一一对应,方便查看后续代码。
形参 | 实参 |
---|---|
hash | hash(key),HashSet中要添加的元素经过计算后的hash值 |
key | key ,也就是最初要添加的元素e,字符串“110” |
value | value HashSet中传入的静态常量PRESENT |
onlyIfAbsent | false |
evict | true |
继续往下走:
Node<K,V>[] tab; Node<K,V> p; int n, i;
声明局部变量,tab为Node<K,V>类型的数组,p是一个Node节点,是一个单链表。n是数组的长度,i是下标(后面会看到)
往下走之前先看一下HashMap中Node的结构,代码如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
可以看出,是一个静态内部类,其结构是一个单链表,实现了Map接口中的嵌套接口Enty。看到这里,就已经明白为何大家都说HashMap为什么底层实现是数组+单链表 了。
继续往下走:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
table是成员变量(上面有提到),这是我们第一次向里面添加元素,所以此时的table是null。然后把table的值赋值给tab,并判断是否为null,结果为真,短路或直接执行if中的语句。调用resize()方法,这个代码是HashMap扩容的方法,实现很复杂,现只摘取本次运行会执行的代码展示如下,并用备注进行说明:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //尚未初始化,table和oldTable都为null
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
省略。。。
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //静态常量,默认容量,16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//根据默认加载因子0.75计算出需要扩容时的容量大小
}
省略n行。。。
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建Node类型的数组,大小为默认容量16
table = newTab;//并赋值给成员变量table
if (oldTab != null) {//oldTable为null,不执行
省略。。。
}
return newTab;//返回newTab,注意:此时成员变量table和newTab指向的是同一个对象
}
resize()方法总结:第一添加元素时,会执行此方法,创建一个Node<>[]数组,长度为16,赋值给成员变量table并返回这个数组的引用
现在继续看之前的代码:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
此时的tab已经指向一个新的,长度为16的Node[]对象(与成员变量table相同),n的值为16。
继续往下走:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
上面代码的意思:
- 根据hash值计算出tab数组中的下标,并把值赋值给p
- 如果p的值为null,执行语句,因为我们是第一次添加,肯定为null。
- 执行语句
tab[i] = newNode(hash, key, value, null);
,存入一个Node节点,根据上面Node节点的构造方法,其对应的分别是:
Node属性 | 传入的值 |
---|---|
hash | key的hash |
key | key值,也就是传入的"110" |
value | 静态常量PRESENT |
next | 下个节点,null |
tab[i = (n - 1) & hash]
,,由于HashMap的长度永远为2的幂,所以这段话就是利用位运算求余,等价于hash%n
,但是效率比直接求余高的多。求余的原因也利用除留余数法进行散列,这样得到的下标就是一个散列的。
继续走:
else {
不执行,稍后第二次添加的时候再说,,此时省略。。。
}
++modCount;
if (++size > threshold) //检查扩容
resize();
afterNodeInsertion(evict);
return null; //添加成功,返回null
回到put方法,同样返回null,回到HashSet的add方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
添加成功,返回true。
到此,我们第一次的add就分析完毕了,继续第二行添加一个重复的对象
添加重复元素
废话不多说,直接跳到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;
if ((tab = table) == null || (n = tab.length) == 0) //此时if语句不执行了,已经不为空,但是依然会赋值,tab为成员变量table,n为长度
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//根据hash计算出下标,由于String的内容一样,所以hashCode一样,所以i的值也与第一次一样,p为对应的下标中的元素。但是不为空了,执行else代码块
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;
/*
*hashMap去重的关键,分析在下面特别摘出
*/
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; //key值已存在,更新对应key值的value
afterNodeAccess(e);
return oldValue; //返回更新之前的value值
}
HashMap去重的关键代码分析:
else {
Node<K,V> e; K k;//此处e值如果为Null,表示添加成功,否则就是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);
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;
}
}
这段代码执行的前提条件:向里面存入元素时,根据hash值算出的table[]的下标,其对应的值不为空,就会执行上面的else代码块。下面逐行进行分析:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
此时p中存储的是table[i]的值,也就是单链表的第一个节点。会先判断存入对象的hash值与当前存储的元素的hash值,如果不相同,直接执行后面的代码。hash值相同后会执行((k = p.key) == key ) || key != null && key.equals(k)))
,这句话先将存入的key值赋值给局部变量k,然后与要添加的元素的key值进行==
比较,相同则表示内存地址一样,key值重复了。否则会进行非空和调用key值的equals方法去比较,如果结果为真,则表示对象相同。把p值赋值给e(在下面的代码if (e != null) { // existing mapping for key
会对e值进行判断,null表示key值不重复,已存入Map中,非空时e值表示的是p的值,也就是已存在的key值重复的节点Node)。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
执行到这里的时候,证明table[i]的第一个节点中的key值不相同,判断是否已经转成红黑树,如果已成转成红黑树,则向下转型为TreeNode去进行判断添加。TreeNode继承自LinkedHashMap中的静态内部类Entry,LinkedHashMap.Entry又继承自HashMap中的静态内部类Node。
继续向下看:
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//更新e值,为下一节点。如果遍历的e值为null时,就表示要添加元素的key值与此链表上的所有元素都不重复
p.next = newNode(hash, key, value, null);//新增节点Node并将要添加的元素存入
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //判断单链表的长度,如果长度大于等于8时,转成红黑树存储
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //如果e值也就是下一节点,中的元素key值重复,直接中断循环,此时e值依旧不为空,表示有存在的key值
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //更新p值,使其指向next节点
}
这段代码块执行的前提条件:table[i]中第一个节点key值重复,而且尚未转成红黑树。循环内容是使用循环进行开始遍历单链表,直到遍历的下一个节点为null或者找到重复的key值为止。下一节点为null时,就表示就表示要添加元素的key值与此链表上的所有元素都不重复,此时将会直接添加一个新的节点,并将数据存入。已存在key值时,直接中断循环进入下面的代码if (e != null) { // existing mapping for key
会对e值进行判断,null表示key值不重复,已存入Map中,非空时e值表示的是p的值,也就是已存在的key值重复的节点Node。
执行完毕,如果有重复的key,会更新key值对应的value值,并返回旧的value值。HashSet中value是一个静态常量,所以都一样。否则返回null,表示数据存入到HashMap中了。