Ⅰ 概述(一~五):先罗列五个情况再分析
一、向集合中添加两个常量池中的String类对象
package test;
import java.util.HashSet;
public class test {
public static void main(String[] args) {
String name1 = "Tom";
String name2 = "Tom";
HashSet<String> set = new HashSet<>();
set.add(name1);
set.add(name2);
System.out.println(set.size());
}
}
结果为1
二、向集合中添加一个常量池中的和一个直接在堆中创建的String类对象
package test;
import java.util.HashSet;
public class test {
public static void main(String[] args) {
String name1 = "Tom";
String name2 = new String("Tom");
HashSet<String> set = new HashSet<>();
set.add(name1);
set.add(name2);
System.out.println(set.size());
}
}
结果为1
三、创建了自定义类(未重写任何方法)
package test;
public class Student {
private String id;
public Student(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
四、创建了自定义类(重写了hashCode方法)
package test;
public class Student {
private String id;
public Student(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
五、创建了自定义类(重写了hashCode方法和equals方法)
package test;
public class Student {
private String id;
public Student(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Student) {
Student student= (Student)obj;
return id.equals(student.getId());
}
return false;
}
三四五共用的测试类(添加自定义类对象)
package test;
import java.util.HashSet;
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);
System.out.println(set.size());
}
}
三四五结果分别为2,2,1
Ⅱ 接下来是详述③和⑤(泛型为自定义类),并讨论③和⑤在Object类下的情况
在向学生管理系统集合中添加元素时,可能会出现输重编号的错误,我们就对这一问题展开叙述,这是一个学生类,有姓名和学号两个封装属性,重写了equals方法,第一次存tom,第二次存tim,学号都是"110",观察代码的运行情况
package bayueshixiawu;
import java.util.HashSet;
public class Student {
private String id;
private String name;
public Student(String name, String id) {
this.name = name;
this.id = id;
}
public Student(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
Student stu = (Student) obj;
return id.equals(stu.id);
}
}
一、当泛型K为Student自定义类时
③.未重写hashCode和equals方法
以下段代码为例(未重写hashCode)
package bayueshixiawu;
import java.util.HashSet;
public class safs {
public static void main(String[] args) {
HashSet<Student> set = new HashSet<>();//map=new HashMap
Student tom = new Student("110");
Student tim = new Student("110");
set.add(tom);
set.add(tim);
System.out.println(set.size());
}
}
下面是涉及的部分方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(1)执行第一个add(tom)时
set.add(tim)相当于map.put(K,V),参数分别属于学生类和Object类,put调用putVal(hash(key),key,value,false,true)
hash就是key.hashCode()值再进行位运算的结果(通俗得说是自定义学生对象的地址的位运算),
如果K类(这里指自定义的学生类)未重写hashCode方法,则调用的是Object的hashCode(),该方法通过native调用本地资源得出对象的地址,因此每新创建的某个学生对象的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;//先行要点:tab与table类型都是数组,但table是全局变量,默认初始值为null
if ((tab = table) == null || (n = tab.length) == 0)//tab==null,进入if
n = (tab = resize()).length;//请移步此代码下面的resize()方法,理解完再回来~//~回来后,tab临时保存table的值
if ((p = tab[i = (n - 1) & hash]) == null)//第一次向集合存元素tab必为空,进入if
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()成功
}
通过下面的resize()方法,可以得到以下信息:
1.返回值是newTab
2.中间有一步将newTab的值赋给了table
3.newTab是通过new Node建立的大小为16的数组
结果是:table保存的值和resize()返回值都是newTab的地址
final Node<K,V>[] resize() {
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;//这个值是16!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
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];//在这里新建了大小为16的Node数组!!!!!!!!!!!!
table = newTab;//在这里赋给了table!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
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;//返回值!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
}
(2)执行第二个add(tim)时
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保存了上一个newTab的地址,其在(n - 1) & hash下标中已有tom,不进入if
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//由于未重写Student的hashCode方法,不同对象的hash值不同,进而下标(n - 1) & hash与第一个不同,不进入if
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))))
//p.hash是上一个tom的,所以不进入if,进入下面的else
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()成功
}
⑤.重写了hashCode和equals方法
hash值从关心对象地址变成了关心id内容是否相同
package bayueshixiawu;
import java.util.HashSet;
public class Student {
private String id;
private String name;
public Student(String name, String id) {
this.name = name;
this.id = id;
}
public Student(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
return id.hashCode();//默认有个this,id是String类的,因此这里的hashCode又是调用String类中的hashCode方法
}
@Override
public boolean equals(Object obj) {
Student stu = (Student) obj;
return this.id.equals(stu.id);
}
}
String类中重写的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;
}
我们来看看有什么变化
这个是主函数
package bayueshixiawu;
import java.util.HashSet;
public class safs {
public static void main(String[] args) {
HashSet<Student> set = new HashSet<>();//map=new HashMap
Student tom = new Student("110");
Student tim = new Student("110");
set.add(tom);
set.add(tim);
System.out.println(set.size());
}
}
这个是hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
首先是hash值,key调用hashCode方法时实际上是Student调用重写后的,因此tom和tim的hash值相同
(1)执行第一个add(tom)时
进入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)//tab==null,进入if
n = (tab = resize()).length;//tab table resize()都是newTab的地址即new Node那个地址
if ((p = tab[i = (n - 1) & hash]) == null)//进入if
tab[i] = newNode(hash, key, value, null);//将tom存储
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()成功
}
(2)执行第二个add(tim)时
在hash方法中会有key调用hashCode方法,是重写的id调用String类的方法
hash值从关心对象地址变成了关心id内容是否相同
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保存了上一个newTab的地址,不进入if
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//id内容相同,hash值相同,进而下标(n - 1) & hash与第一个相同,不为空,不进if
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))))
//两个学生的hash相同(只关心内容),两个对象地址不同但id内容相同(equals重写)
e = p;//把上一个对象地址赋给e
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;//返回oldValue,必不为null,add()失败
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;//add()成功
}
HashSet set = new HashSet<>()
设HashSet集合中存储类型为K,点进HashSet
public HashSet() {
map = new HashMap<>();
}
因此new HashSet<>()相当于 map=new HashMap<>(),(map是HashSet中的一个HashMap类的全局变量)
也就是说HashSet的add方法其实是HashMap的。都在用一个add方法
总结最初5个问题的原因
①.
Tom.hashCode()相同,这里调用的是String重写后的方法,hash值相同,两对象地址相同,第二次添加时在putVal中进入第一个else的第一个if,然后return了oldValue,添加失败
②.
根据String的含有一个参数的构造方法可知,这里将"Tom"的hash值直接赋给了name2
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
package test;
import java.util.HashSet;
public class test {
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public static void main(String[] args) {
String name1 = "Tom";
String name2 = new String("Tom");
HashSet<String> set = new HashSet<>();
set.add(name1);
set.add(name2);
System.out.println(set.size());//1
System.out.println(hash(name1)==hash(name2));//true
}
}
hash值相同,name2.equals(name1),equals方法先比较地址再比较内容,有一个符合,值为true,第二次添加时在putVal中不进第二个else也就不会创建新的Node节点去存储name2,结果就是1
③.
在hash方法中调用Object类的hashCode方法,由于tom和jim两对象地址不同,hash值不同,第二次添加时在putVal中进入第二个if,创建了新的Node节点去存储jim
④.
在hash方法中调用自定义类重写后的hashCode方法,id为String类,又调用String类重写后的hashCode方法(比对内容),内容相同,hash值相同,第二次添加时在putVal中的第一个else中执行到key.equals(k)时会调用Object类的equals方法(比对对象地址),对象地址不同,第二次添加时在putVal中进入第二个else创建了新的Node节点去存储jim
⑤.
在hash方法中调用自定义类重写后的hashCode方法,id为String类,又调用String类重写后的hashCode方法(比对内容),内容相同,hash值相同,第二次添加时在putVal中的第一个else中执行到key.equals(k)时会调用自定义类重写的equals方法(比对对象的id属性的内容),id属性的内容相同,进入第一个else的第一个if然后return了oldValue,添加jim失败
二、当泛型K为Object类时
我们为了防止因为编号相同的狗和人两个不同的类被视为同一元素不被添加入集合,所以在实现了防止同类重复(A狗110和B狗110)之后需要进行怎样的操作才能实现不同类同编号(狗110和人110)不起冲突呢
先让狗类也实现判重功能,重写hashCode和equals方法
加入添加A狗110和B狗110
1.重写了hashCode后id内容相同hash值就相同
2.重写了equals后关心id,否则就比对两对象key的地址,然后把错误的(B狗110)添加到集合
package test;
public class Dog {
private String id;
public Dog(String id) {
this.id=id;
}
@Override//,
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
return this.id.equals(id);
}
}
package test;
public class Student {
private String id;
public Student(String id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
Student student= (Student)obj;
return this.id.equals(student.id);
}
}
此时会发现无论先添加哪个类型都是错误的:
1.先输入人再输入狗:
package test;
import java.util.HashSet;
public class test {
public static void main(String[] args) {
Student tom = new Student("110");
Dog dog = new Dog("110");
HashSet<Object> set = new HashSet<>();
set.add(tom);
set.add(dog);
System.out.println(set.size());
}
}
结果:
2.先输入狗再输入人
package test;
import java.util.HashSet;
public class test {
public static void main(String[] args) {
Student tom = new Student("110");
Dog dog = new Dog("110");
HashSet<Object> set = new HashSet<>();
set.add(dog);
set.add(tom);
System.out.println(set.size());
}
}
结果:
以先添加人为例
因为无论先添加谁只要前后类型不一致,在添加后一个时在执行putVal方法中的以下代码段(633行)
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//key为狗
e = p;
时会调用后一个添加的对象的equals方法,即调用狗的equals方法
@Override
public boolean equals(Object obj) {//obj为人
Dog dog = (Dog) obj;//!!!问题出在这里
return this.id.equals(dog.getId());
}
导致错误的原因:人与狗类型不能互转,但不转的话下面调用不了getId方法,互相矛盾,因此报错
改:确保调用equals时必能下转型成功
要解决这样的问题需要在重写的equals方法加一个条件判断传入的对象是否与之前传入的为一个类型,如果是不同类型的元素,会返回false,在putVal方法中就会开辟新的空间去存放此元素
@Override
public boolean equals(Object obj) {//重写了这里后比id,不然就比两对象key的地址,然后就能添加那个错误的了
if(obj instanceof Dog) {
Dog dog = (Dog) obj;
return this.id.equals(dog.getId());
}
return false;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Student) {
Student student= (Student)obj;
return this.id.equals(student.getId());
}
return false;
}
key是否重复,是由两个步骤判断的:
hashcode是否一样
如果hashcode不一样,就是在不同的坑里,一定是不重复的
如果hashcode一样,就是在同一个坑里,还需要进行equals比较
如果equals一样,则是重复数据
如果equals不一样,则是不同数据。