首先创建一个学生类
class Student{ String id; public Student(String id) { this.id = id; } }
然后向HashSet集合存储学生类型的对象:
HashSet<Student>set = new HashSet<>(); set.add(new Student("1")); set.add(new Student("1"));
分析底层代码:
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); }
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; }
根据上一篇博客:《HashSet中的add方法(对集合底层的分析)》https://blog.youkuaiyun.com/qq_40831381/article/details/90119935 由此可知:
执行第一行代码过程---->调用HashSet无参构造方法---->hashCode无参构造方法----->可知HashMap中table变量为null。
执行第二行代码过程---->tab=resize();方法为table变量赋值---->该值即为该方法的返回值---->tab局部变量与table全局变量指向同一个数组,数组长度是16,向set集合中成功添加第一条数据
执行第三行代码过程---->由于new Student("1")和new Student("1")对象的地址不同,所有依旧能向set集合中成功添加第二条数据
重写hashCode
在上面写的代码中,由于Student类中并没有重写hashCode()方法,所以在调用hash(Object key)时,进而调用的是object中hashCode方法:(return super.hashCode();)比较的是传入对象的地址值,又测试代码中add()方法传入的两个对象的地址值不相同,故hash值不同。由上一篇博客的分析可知,在计算机中,这两个对象都可以添加进HashSet集合中。但是我们人为的希望比较的是Student类中的id是否相同,id相同视为同一个学生对象,实现不能重复添加id相同的Student对象的功能,即不需要比较地址是否一致,只要是值一致即可。这时可以通过在自定义的Student类中重写hashCode()方法来实现。代码如下:
@Override public int hashCode() { return id.hashCode(); }
这时就会调用我们重写的这个该hashCode()方法实质上是在调用id的hashCode方法,而id是String类型的,故实际上调用的是String类中的hashCode()方法,比较内容是否相同,上述代码中创建的Student对象中所传入的id内容相同,那么hash值相同,而不再是调用object中的hashCode方法。保证了(p = tab[i = (n - 1) & hash])不为空,执行else分支
具体执行过程:
第二行代码set.add(new Student(“1”))的执行过程1.程序执行 if ((tab = table) == null || (n = tab.length) == 0)语句代码块,tab=resize();方法为table变量赋值,此处的table是在HashMap类中定义的成员变量,由于在声明时未显示初始化,HashMap类中的所有构造方法当中也没有对table变量赋值,故该值默认为null。故条件成立,执行if代码块中的n = (tab = resize()).length语句,resize()方法执行完后会返回一个默认初始长度为16的数组,故tab和table都指向该数组。
2.程序继续执行if ((p = tab[i = (n - 1) & hash]) == null)语句,此处的i值是根据所传key的hash值对tab数组空间的一个映射,也就是(n - 1) & hash为数组下标。此时tab[i]中没有数据,if语句成立,执行tab[i] = newNode(hash, key, value, null),创建一个Node对象(封装id="1"的Student对象)并存到tab[i]中,也就是说此时tab(table)指向的数组中已经存储了一个id="1"的Student对象。
3.程序执行了if后 跳过else语句执行下面代码,返回null。即putVal方法返回null,put方法也返回null,add方法返回true,添加成功。
第三行代码set.add(new Student(“1”))的执行过程1.程序执行 if ((tab = table) == null || (n = tab.length) == 0)语句,由于此处的tab(table)指向的数组中已经存储了一个new Student("1")的对象,不为空,故条件不成立,不再继续执行if代码块中的内容。
2.程序继续执行if ((p = tab[i = (n - 1) & hash]) == null)语句,由之前的分析可知此处的hash值与执行第二行代码时的hash值相同,故i值不变,即tab[i]中存储的是id="1"的Student对象,变量p指向tab[i],保证了p = tab[i = (n - 1) & hash])不为空,故条件不成立,跳过if代码块,执行else代码块中的内容。
3.程序继续执行 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))语句,p.hash == hash语句实质是在比较第一次传入的Student对象的hash值和本次传入的Student对象的hash值,由之前的分析可知为true,但是(k = p.key) == key中,k=p. key即k和p中存储的都是第一次传入的Student对象的地址,而key是本次传入的Student对象的地址,显然不同,故而执行key != null && key.equals(k),key!=null成立执行key.equals(k),由于key是自定义的Student类,并没有重写Object类中的equals方法,所以此时调用的仍然是Object类中的equals方法,比较地址值是否相同,结果为false,而如果要实现人为的希望比较的是Student类中的id是否相同的功能,此处结果必须为true,所以需要在Student类中重写equals方法。
重写equals方法
@Override public boolean equals(Object obj) { Student student = (Student)obj; return id.equals(student.id); }
重写equals方法之后,此处实际上是调用Student类中的重写后的equals方法,而该方法实际是调用id的equals方法,而id是String类型的,故实际上调用的是String类中的equals()方法,比较内容是否相同,显然为true。条件成立,执行e = p。
此时,程序执行if (e != null) {…}代码块中的内容,返回一个不为null的值,即putVal方法返回值不为null,put方法返回值也不为null,add方法返回false,添加失败。实现了人为的希望比较的是Student类中的id是否相同的功能,即不能重复添加id相同的Student对象的功能。当泛型为Object的时候传入其他的非Student类的时候有可能会出现异常
为了防止存入不同的对象,需要使用 instanceof,代码如下:
@Override public boolean equals(Object obj) { //return super.equals(obj); if (obj instanceof Student) { Student student = (Student)obj; return id.equals(student.id); } return false; }
总结:
1.hashCode和equals的执行顺序:先执行hashCode方法,在hashCode相等的情况下,再执行equals方法
2.重写equals方式时,注意使用instanceof