声明:本博客中所有底层代码片段均按照JDK 8提供src源代码缩进方式缩进
目录
分析前须知
1、 add()方法是什么?
add()方法用于向集合中添加元素(具体使用方法可参考:HashSet类常用方法总结)
2、add()调用了哪些方法?
首先创建一个测试类,创建set对象调用add方法
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet<String> set = new HashSet();
set.add("Tom");
}
}
使用编辑器“Ctrl + 单击add()方法”快捷方式直接进入add()方法所属HashSet.class类中找到如下片段
==========底层代码片段1==========
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
我们发现add方法的实现是先通过使用Map类型对象调用put()方法实现的,接下来再次使用“Ctrl + 单击put”快捷方式进入到方法put()所属类,得到如下代码段
==========底层代码片段2==========
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
}
在底层代码片段2中我们可以看出,put方法又调用了putVal()方法,而在putVal()方法的参数中调用了hash方法,使用“Ctrl + 单击hash()方法”快捷方式到达hash()方法所在位置
==========底层代码片段3==========
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
发现hash方法返回一个调用hashCode()方法获得的值
同样,我们对hashCode()方法进行溯源,结果进入到Object类中,发现hashCode方法使用了C语言底层代码,这里不再分析片段,其作用是根据对象获取一串由公式计算出的数字代表该对象在内存中的地址(并不是地址),本博客在下面会借助其它途径证明。
至此我们已经大概了解hash()作用就是根据传递的参数key返回一个类似地质作用的识别码。
再次返回到底层代码片段2中
我们进入putVal()方法中,发现创建了Node<K,V>类型的tab数组和p变量(均为局部变量、数组),此处仅需知晓其用于存放代表我们使用add方法传入的参数“Tom”即可。
在接下来的一行里,我们看到“table”,这里仅需知道它和tab是同一类型也用于存放相关参数,并且是全局变量即可。
再往下一行,我们可以看到putVal方法体中又调用了resize()方法,这里不再对其进行详细分析,仅需知道resize()方法可以新创建一个Node<K,V>类型数组可以存放元素参数即可。
至此,除基本方法以及部分对分析add()影响不大的方法外,本博客需要使用的方法已全部找到。
分析hashCode()方法作用
1、一般情况
实例如下
public class Test {
public static void main(String[] args) {
Test test = new Test();//创建Test类对象
System.out.println(test.hashCode());//调用hashCode方法获取特征码
System.out.println(Integer.toHexString(test.hashCode()));//将特征码转换为16进制
System.out.println(test);//打印test对象在内存中的地址
}
}
运行结果如下:
5433634
52e922
set.Test@52e922
从上面例子中可以看出,hashCode()方法确实是根据对象进行计算并返回一个数值,该数值转换为16进制后,与对象地址后半部分相同,即hashCode()生成的特征码一定程度上可以代表地址。
2、特殊情况
我i们都知道,两个对象在内存中的地址一定不相同,根据上面的举例,又可以推断出这两个对象生成的hashcode码也应该不相同。
但如果是两个字符串类型对象又会如何
public class Test {
public static void main(String[] args) {
String name1 = new String("Tom");
String name2 = new String("Tom");
System.out.println(name1 == name2);//判断name1和name2字符串对象在内存中的地址是否相同
System.out.println(name1.hashCode());//打印name1的hashcode码
System.out.println(name2.hashCode());//打印name2的hashcode码
}
}
运行结果如下:
false
84274
84274
根据以上结果可以说明,两字符串对象在内存中的地址确实不同,但是两者生成的hashcode码确相同。
这是因为,String类重写了hashCode()方法,name1和name2调用的是重写过的hashcode方法。
使用Ctrl+ 单击hashcode方法,定位到方法所在位置,其源代码如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
从上面源码中大致可以发现,String类重写的hashCode()方法是根据字符串的每个字符而计算出的hashcode码,也正因此,字符串相同的String对象,即使地址不同,其生成的hashcode码相同。
通过实例分析add()方法底层代码
实例一:如何判断是否重复(两对象地址相同)
我们知道HashSet集合中是不能存放相同的元素的,在本博客前部分已经说明,向hashSet中新增元素其实调用的是hashMap中的put方法,而HashMap集合中不允许有相同元素存在,这是由Map集合的的定义决定的(可以参考:HashMap常用方法总结)。因此若使用add()方法向集合中存放已经存在的元素,则会存放失败并返回false,那么add()方法是如何实现判断集合中是否存在与当前要加入的元素重复的元素呢?
实例
import java.util.HashSet;
import java.util.Iterator;
public class Test {
public static void main(String[] args) {
HashSet<String> set = new HashSet();
set.add("Tom");
System.out.println(set.add("Tom"));
}
}
运行结果为:
false
分析底层代码如下:
==========底层代码片段1==========
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
==========底层代码片段2==========
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
}
①第一次使用add()方法向set集合中添加元素时
1.add将Tom作为参数传递给put方法
return map.put(e, PRESENT)==null;
2.put方法将其参数传递给putVal方法,字符串类型对象“Tom”调用hash()方法,即调用String类重写过的hashCode()方法,返回一个hashcode码
return putVal(hash(key), key, value, false, true);
3.进行第一次if判断,由于tab与table还没被赋值,所以为空,满足if条件,执行下面的语句,使用resize()方法创建一个新的Node<K,V>类型的数组并将地址传递给全局变量table,并且tab与table指向的是同一对象
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
4.执行第二个if判断,判断tab数组中第i=(n-1)&hash个元素是否为null(注意hash是获取hashcode码的方法,上面已经介绍过),并将该元素赋给p。此时由于还没向数组中添加元素,数组为空,因此执行下一行代码,把put方法传递的代表Tom的参数存放至数组tab的第i个空间(此时已经成功向集合中添加Tom元素)。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
5跳过else执行接下来的代码,我们忽略前四行,最终的返回值是null,即add()调用put再调用putVal最终得到并返回结果null
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
6.最后返回add方法,由于add返回值是null,因此map.put(e,PRESENT) ==null成立,返回true,即向集合中添加元素成功
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
②第二次使用add()方法向set集合中添加相同元素时
1.add将Tom作为参数传递给put方法
return map.put(e, PRESENT)==null;
2.put方法将其参数传递给putVal方法,第二次添加的元素的字符串类型对象“Tom”调用hash()方法,即调用String类重写过的hashCode()方法,返回hashcode码,由于是string重写过的hashCode()方法,因此只要字符串相同,其hashcode码就相同。
return putVal(hash(key), key, value, false, true);
3.进行第一次if判断,将table存放的地址传递给tab,由于table中存放的是上一个Tom元素不为空,tab的长度也不等于0,因此跳过第一个if判断执行第二个if判断
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
4.由于第二次添加元素与第一次相同,因此在调用hash方法时产生的hashcode码也就相同,因此此处的i的值与上一个次添加tom时的i的值相同,因此两次的p亦即tab[i]指向的是同一个位置,而这次tab[i]不再为空,不满足if判断条件,因此不执行赋值语句(也就意味着赋值失败),执行下面的else语句
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
5.进入else语句后先执行第一个if判断语句,首先判断p(指向上一次添加的Tom的参数在数组tab里的位置)的hashcode是否与这次添加元素的hashcode相同。再看逻辑与符号右边,(k = p.key) == key 用于判断两次传入的key值(即Tom)在内存中的地址是否相同,由于Tom是String类型,因此在第二次使用add()方法时,编译器检测到Java的堆中存在“Tom”字符串,于是将同一个地址赋给了第二次添加的Tom元素,两地址相等,返回true,不再进行下面的判断,直接执行e = p,现在e也指向tab数组中第一次添加的Tom元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
6.跳过else if和else执行下一个if判断语句。e确实不为null,执行下面语句,将e中的value值(前面add方法中已经赋常量PRESENT)赋给oldValue,接着判断if (!onlyIfAbsent || oldValue == null),在底层代码片段2的第二行中,已经将onlyIfAbsent赋为false,此时取反得true,故执行下面语句,将当前的value值(依然是常量PRESENT)赋给上一次添加元素Tom的value,实现了value覆盖。最后return oldValue,因此putVal也即put得到的返回值就是oldValue
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
7.add得到的返回值不为null,最终返回false,即表示添加失败
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
实例二:如何判断是否重复(两对象地址不同)
实例
import java.util.HashSet;
import java.util.Iterator;
public class Test {
public static void main(String[] args) {
HashSet<String> set = new HashSet();
set.add("Tom");
set.add(new String("Tom"));
}
}
上面代码中在添加第二个元素时,创建了新的对象,因此其地址不再与第一个“Tom”的地址相同
本次实例二与上次实例一的不同从第二次使用add()方法向set集合中添加相同元素在else中进行第一次判断时开始【即实例一:如何判断是否重复(两对象地址相同)——>②第二次使用add()方法向set集合中添加相同元素时——>5.进入else语句后先执行第一个if判断语句,首先判断p(指向上……】,因此,分析步骤从第二次添加元素执行else中第一次判断开始
分析如下:
1.进入else语句后先执行第一个if判断语句,首先判断p(指向上一次添加的Tom的参数在数组tab里的位置)的hashcode是否与这次添加元素的hashcode相同。再看逻辑与符号右边,(k = p.key) == key 用于判断两次传入的key值(即Tom)在内存中的地址是否相同,两次地址不同,逻辑或左边不满足;判断右边,key值确实不为null,并且这次的key与上一个添加的元素Tom的key值使用equals()比较,完全相同,成立,因此可以继续执行e = p;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
而接下来的步骤就和实例一中第二次添加元素接下来的步骤相同,最终add的返回值为false,添加失败。
实例三:能否判断是否重复(自定义学生类学号相同)
我们现在定义学生类如下:
public class Student {
private String id;
public Student(String id) {//定义有参构造方法
this.id = id;
}
}
测试类如下:
import java.util.HashSet;
import java.util.Iterator;
public class Test {
public static void main(String[] args) {
Student Tom = new Student("110");
Student Jim = new Student("110");
HashSet<Student> set = new HashSet();
set.add(Tom);
set.add(Jim);
}
}
那么此时add()方法能否判断出两自定义类的对象其学号相同从而在第二次添加时返回false?
下面我们继续来分析底层代码:
==========底层代码片段1==========
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
==========底层代码片段2==========
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
}
①第一次使用add()方法向set集合中添加元素时
1.add将Tom作为参数传递给put方法
return map.put(e, PRESENT)==null;
2.put方法将其参数传递给putVal方法 ,Student类型的Tom调用hash()方法,由于Student类没有重写hash方法中的hashCode()方法,因此调用的是student父类Object类的hashCode()方法,根据对象在内存中的地址返回hashcode码
return putVal(hash(key), key, value, false, true);
3.进行第一次if判断,由于tab与table还没被赋值,所以为空,满足if条件,执行下面的语句,使用resize()方法创建一个新的Node<K,V>类型的数组并将地址传递给全局变量table,并且tab与table指向的是同一对象
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
4.执行第二个if判断,判断tab数组中第i=(n-1)&hash个元素是否为null(注意hash是获取hashcode码的方法,上面已经介绍过),并将该元素赋给p。此时由于还没向数组中添加元素,数组为空,因此执行下一行代码,把put方法传递的代表Tom的参数存放至数组tab的第i个空间(此时已经成功向集合中添加Tom元素)。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
5跳过else执行接下来的代码,我们忽略前四行,最终的返回值是null,即add()调用put再调用putVal最终得到并返回结果null
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
6.最后返回add方法,由于add返回值是null,因此map.put(e,PRESENT) ==null成立,返回true,即向集合中添加元素成功
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
②第二次使用add()方法向set集合中添加相同元素时
1.add将Jim作为参数传递给put方法
return map.put(e, PRESENT)==null;
2.put方法将其参数传递给putVal方法,第二次添加的元素的Student类型的Jim调用hash()方法,由于Student类没有重写hash方法中的hashCode()方法,因此调用的是student父类Object类的hashCode()方法,根据对象在内存中的地址返回hashcode码,由于两次创建的Student类型对象Tom和Jim在内存中地址一定不同,那么它们的hashcode码也一定不同。
return putVal(hash(key), key, value, false, true);
3.进行第一次if判断,将table存放的地址传递给tab,由于table中存放的是上一个Tom元素不为空,tab的长度也不等于0,因此跳过第一个if判断执行第二个if判断
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
4.上面已经说明,Student类型的Tom和Jim对象由于Student类没有重写hashCode()方法,因此两者产生的hashcode码是不同的。tab数组中第i=(n-1)&hash个元素因为还没有被赋值过,因此一定为null,接着执行下一行代码,把put方法传递Student类型Jim对象的参数存放至数组tab的第i个空间(此时已经成功向集合中添加Jim元素)。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
5跳过else执行接下来的代码,我们忽略前四行,最终的返回值是null,即add()调用put再调用putVal最终得到并返回结果null
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
6.最后返回add方法,由于add返回值是null,因此map.put(e,PRESENT) ==null成立,返回true,即向集合中添加元素成功
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
思考
在上面的实例三中,add()方法并没有达到我们的期望——判断自定义Student类的对象学号是否相同,那么我们来思考如何才能实现add()识别学号呢?
尝试一:重写hashCode()方法
我们将实例三与实例二对比发现,两次实例的两个对象地址都不相同,但实例二在使用hash()方法产生hashcode吗码时,其调用的是String类中被重写过的hashCode()方法,因此两次添加元素的hashcode码都相同;而实例三中使用hash()方法调用的是Student类中继承父类Object的hashCode()方法,得到的两个对象的hashcode码一定不同。
那么如果能重写Student类中的hashCode()方法,使其能够判断对象的学号id而不是两个对象的地址,这样能否实现add()避免学号重复的效果呢?
重写Student类中的hashCode()方法如下:
public class Student {
private String id;
public Student(String id) {
this.id = id;
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
return id.hashCode();//返回的是String类型的id属性的hashcode码
}
}
前面分析过程不再过多赘述,这里直接跳到判断hash值的过程
分析:
1.进入else语句后先执行第一个if判断语句,由于重写了hashCode()方法,因此返回的两次id的hashcode码相同,逻辑与运算符左边为true。逻辑与符号右边,两次key的地址一定不同,逻辑或左边不满足;判断右边,key值确实不为null。这次添加的元素Jim的key与上个元素Tom的Key调用equals方法比较,由于Student类没有重写equals方法,因此实际调用的是继承父类Object的equals方法,判断的不是字符串而是地址,然而两地址一定不同,故这个if判断语句结果为false,执行下一个else if语句。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
2.判断p是否属于TreeNode类型或其子类,这里结果是不属于。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
3.遍历tab数组,如果前一次存入元素的下一个位置为空,那么就将本次要添加的元素的参数存放到其中,并跳出循环。接着执行下一个if判断语句,无论结果如何,最后将e的地址传递给p(此时的e和p都指向第一次存放Tom元素的参数在tab数组中的位置)。
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.接着执行下面语句,最后putVal()方法返回null
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
5.最后返回add方法,由于add返回值是null,因此map.put(e,PRESENT) ==null成立,返回true,即向集合中添加元素成功
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
【尝试失败!】
尝试二:重写hashCode()方法和equals()方法
我们发现,如果刚刚代码分析中“分析1”中if语句判断条件满足时,就可以返回一个非null的值最终add方法返回false。
思考发现,为了实现判断Student类型对象的id属性,就不能使用Object类中的equals()方法,而是需要重写一个能够判断Student类id属性的equals()方法。下面我们来重写Student类中的equals()方法:
public class Student {
private String id;
public Student(String id) {
this.id = id;
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
return id.hashCode();//返回的是String类型的id属性的hashcode码
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Student) {
Student student = (Student) obj;//将obj下转型为Student类
return this.id.equals(student.id);//返回id调用String类型的equals()方法对比学号是否相同的结果
}
return false;
}
}
此时我们在回到刚刚尝试一中的分析第一步
1.进入else语句后先执行第一个if判断语句,由于重写了hashCode()方法,因此返回的两次id的hashcode码相同,逻辑与运算符左边为true。逻辑与符号右边,两次key的地址一定不同,逻辑或左边不满足;判断右边,key值确实不为null。这次添加的元素Jim的key与上个元素Tom的Key调用Student类中重写过的equals方法比较,比较的是字符串id,由于两个元素的id确实相同,因此返回true。if判断语句为true,执行e = p。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
2.跳过else if和else执行下一个if判断语句。e确实不为null,执行下面语句,将e中的value值(前面add方法中已经赋常量PRESENT)赋给oldValue,接着判断if (!onlyIfAbsent || oldValue == null),在底层代码片段2的第二行中,已经将onlyIfAbsent赋为false,此时取反得true,故执行下面语句,将当前的value值(依然是常量PRESENT)赋给上一次添加元素Tom的value,实现了value覆盖。最后return oldValue,因此putVal也即put得到的返回值就是oldValue
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
.3add得到的返回值不为null,最终返回false,即表示添加失败
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
【尝试成功!】