为什么在重写 equals方法的同时必须重写 hashcode方法

 更新时间:2016年07月27日 16:26:51   作者:屌丝的烦恼  
Object 类是所有类的父类,其 equals 方法比较的是两个对象的引用指向的地址,hashcode 是一个本地方法,返回的是对象地址值。他们都是通过比较地址来比较对象是否相等的

我们都知道Java语言是完全面向对象的,在java中,所有的对象都是继承于Object类。
其 equals 方法比较的是两个对象的引用指向的地址,hashcode 是一个本地方法,返回的是对象地址值。Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。

为何重写 equals方法的同时必须重写 hashcode方法呢

可以这样理解:重写了 equals 方法,判断对象相等的业务逻辑就变了,类的设计者不希望通过比较内存地址来比较两个对象是否相等,而 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 的效率。

相关文章

  • Mybatis-Plus的saveOrUpdateBatch(null)问题及解决

    Mybatis-Plus的saveOrUpdateBatch(null)问题及解决

    这篇文章主要介绍了Mybatis-Plus的saveOrUpdateBatch(null)问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • Spring Boot Event Bus用法小结

    Spring Boot Event Bus用法小结

    Spring Boot Event Bus是Spring框架中事件驱动编程的一部分,本文主要介绍了Spring Boot Event Bus用法小结,感兴趣的可以了解一下
    2023-09-09
  • Java集合的定义与Collection类使用详解

    Java集合的定义与Collection类使用详解

    这篇文章主要介绍了Java集合的定义及Collection工具类使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-11-11
  • 详解Spring中@Autowired注解是如何实现的

    详解Spring中@Autowired注解是如何实现的

    在使用java config的过程当中,我们不可避免地会有各种各样的注解打交道,其中,我们使用最多的注解应该就是@Autowired注解了,这篇文章就来和大家聊聊它到底怎么实现的吧
    2023-07-07
  • SpringBoot实现PDF添加水印的示例

    SpringBoot实现PDF添加水印的示例

    本文主要介绍了SpringBoot实现PDF添加水印的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • JVM完全解读之GC日志记录分析

    JVM完全解读之GC日志记录分析

    这篇文章主要介绍了JVM完全解读之GC日志记录分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-01-01
  • java JDBC系列教程之JDBC类的简析与JDBC的基础操作

    java JDBC系列教程之JDBC类的简析与JDBC的基础操作

    这篇文章主要介绍了java JDBC系列教程之JDBC类的简析与JDBC的基础操作,本文分步骤通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07
  • 这一次搞懂Spring自定义标签以及注解解析原理说明

    这一次搞懂Spring自定义标签以及注解解析原理说明

    这篇文章主要介绍了这一次搞懂Spring自定义标签以及注解解析原理说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • Java数据结构之栈的线性结构详解

    Java数据结构之栈的线性结构详解

    从数据结构上看栈和队列都是线性表,不过是两种特殊的线性表,栈只允许在的一端进行插人或删除操作,而队列只允许在表的一端进行插人操作、而在另一端进行删除操作,这篇文章主要给大家介绍了关于Java数据结构之栈的线性结构的相关资料,需要的朋友可以参考下
    2021-08-08
  • Java多线程中ThreadLocal解读

    Java多线程中ThreadLocal解读

    这篇文章主要介绍了Java多线程中ThreadLocal解读,  多线程访问同一个共享变量的时候容易出现并发问题,因此为了保证线程安全性,我们都会采用加锁的方式,而ThreadLocal是除加锁方式之外的另一种保证线程安全性的方法,需要的朋友可以参考下
    2023-09-09

最新评论