java编程里有关约定:如果两个对象根据equals方法比较是相等的,那么调用这两个对象的任意一个hashcode方法都必须产生相同的结果。
因为没有重写hashcode而导致违反了这一条约定。
举个栗子。
在学校中,是通过学号来判断是不是这个人的。
下面代码中情景为学籍录入,学号 123 被指定给学生 Tom,学号 456 被指定给学生 Jerry,学号 123 被失误指定给 Lily。而在录入学籍的过程中是不应该出现学号一样的情况的。
根据情景需求是不能添加重复的对象,可以通过 HashSet 实现。
-
public class Test {
-
public static void main(String[] args) {
-
Student stu = new Student(123,"Tom");
-
HashSet<Student> set = new HashSet<>();
-
set.add(stu);
-
set.add(new Student(456, "Jerry"));
-
set.add(new Student(123, "Lily"));
-
Iterator<Student> iterator = set.iterator();
-
while (iterator.hasNext()) {
-
Student student = iterator.next();
-
System.out.println(student.getStuNum() + " --- " + student.getName());
-
}
-
}
-
};
-
class Student {
-
private int stuNum;
-
private String name;
-
public Student(int stuNum,String name){
-
this.stuNum = stuNum;
-
this.name = name;
-
}
-
public int getStuNum() {
-
return stuNum;
-
}
-
public String getName() {
-
return name;
-
}
-
@Override
-
public boolean equals(Object obj) {
-
if(this==obj)
-
return true;
-
if(obj instanceof Student){
-
if(this.getStuNum()==((Student)obj).getStuNum())
-
return true;
-
}
-
return false;
-
}
输出为:
123 --- Lily
456 --- Jerry
123 --- Tom
根据输出我们发现,再次将学号 123 指定给 Lily 居然成功了。到底哪里出了问题呢?
我们看一下 HashSet 的 add 方法:
-
public boolean add(E e) {
-
return map.put(e, PRESENT)==null;
-
}
其实 HashSet 是通过 HashMap 实现的,由此我们追踪到 HashMap 的 put 方法:
-
public V put(K key, V value) {
-
if (table == EMPTY_TABLE) {
-
inflateTable(threshold);
-
}
-
if (key == null)
-
return putForNullKey(value);
-
int hash = hash(key);
-
int i = indexFor(hash, table.length);
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
-
Object k;
-
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
-
V oldValue = e.value;
-
e.value = value;
-
e.recordAccess(this);
-
return oldValue;
-
}
-
}
-
modCount++;
-
addEntry(hash, key, value, i);
-
return null;
-
}
1.根据 key,也就是 HashSet 所要添加的对象,得到 hashcode,由 hashcode 做特定位运算得到 hash 码;
2.利用 hash 码定位找到数组下标,得到链表的链首;
3.遍历链表寻找有没有相同的 key,判断依据是 e.hash == hash && ((k = e.key) == key || key.equals(k))。在add Lily 的时候,由于重写了 equals 方法,遍历到 Tom 的时候第二个条件应该是 true;但是因为 hashcode 方法还是使用父类的,故而 Tom 和 Lily的 hashcode 不同也就是 hash 码不同,第一个条件为 false。这里得到两个对象是不同的所以 HashSet 添加 Lily 成功。
总结出来原因是没有重写 hashcode 方法,下面改造一下:
-
public class Test {
-
public static void main(String[] args) {
-
Student stu = new Student(123,"Tom");
-
HashSet<Student> set = new HashSet<>();
-
set.add(stu);
-
set.add(new Student(456, "Jerry"));
-
set.add(new Student(123, "Lily"));
-
Iterator<Student> iterator = set.iterator();
-
while (iterator.hasNext()) {
-
Student student = iterator.next();
-
System.out.println(student.getStuNum() + " --- " + student.getName());
-
}
-
}
-
};
-
class Student {
-
private int stuNum;
-
private String name;
-
public Student(int stuNum,String name){
-
this.stuNum = stuNum;
-
this.name = name;
-
}
-
public int getStuNum() {
-
return stuNum;
-
}
-
public String getName() {
-
return name;
-
}
-
@Override
-
public boolean equals(Object obj) {
-
if(this==obj)
-
return true;
-
if(obj instanceof Student){
-
if(this.getStuNum()==((Student)obj).getStuNum())
-
return true;
-
}
-
return false;
-
}
-
@Override
-
public int hashCode() {
-
return getStuNum();
-
}
-
}
输出:
456 --- Jerry
123 --- Tom
重写了 hashcode 方法返回学号。OK,大功告成。
有人可能会奇怪,e.hash == hash && ((k = e.key) == key || key.equals(k)) 这个条件是不是有点复杂了,我感觉只使用 equals 方法就可以了啊,为什么要多此一举去判断 hashcode 呢?
因为在 HashMap 的链表结构中遍历判断的时候,特定情况下重写的 equals 方法比较对象是否相等的业务逻辑比较复杂,循环下来更是影响查找效率。所以这里把 hashcode 的判断放在前面,只要 hashcode 不相等就玩儿完,不用再去调用复杂的 equals 了。很多程度地提升 HashMap 的使用效率。
所以重写 hashcode 方法是为了让我们能够正常使用 HashMap 等集合类,因为 HashMap 判断对象是否相等既要比较 hashcode 又要使用 equals 比较。而这样的实现是为了提高 HashMap 的效率。
二:对象equals 和hashcode的关系
结论:当对象的equals()方法被重写时,通常有必要重写 hashCode() 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。如下:
(1)两个对象相等,hashcode一定相等
(2)两个对象不等,hashcode不一定不等
(3)hashcode相等,两个对象不一定相等
(4)hashcode不等,两个对象一定不等