重写hashCode()方法和equals()方法

本文深入探讨了hashCode()和equals()方法的重要性,解释了为何及何时需要重写这两个方法,以及如何正确地进行重写。强调了在集合操作中,如HashSet和HashMap中,正确实现这两个方法的关键作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

本篇文章主要探讨的问题有三个:

1、首先我们为什么需要重写hashCode()方法和equals()方法? 
2、在什么情况下需要重写hashCode()方法和equals()方法? 
3、如何重写这两个方法?

二、为什么需要重写hashCode()方法和equals()方法

首先,为什么要重写equals()方法。我们在定义类时,我们经常会希望两个不同对象的某些属性值相同时就认为他们相同,所以我们要重写equals()方法,按照原则,我们重写了equals()方法,也要重写hashCode()方法。

Java中的超类Object类中定义的equals()方法是用来比较两个引用所指向的对象的内存地址是否一致,Object类中equals()方法的源码:

public boolean equals(Object obj) {
       return (this == obj);
}

Object类中hashCode()方法的源码:

public native int hashCode();

Object类中的hashCode()方法,用的是native关键字修饰,说明这个方法是个原生函数,也就说这个方法的实现不是用java语言实现的,是使用c/c++实现的,并且被编译成了DLL,由java去调用,jdk源码中不包含。对于不同的平台它们是不同的,java在不同的操作系统中调用不同的native方法实现对操作系统的访问,因为java语言不能直接访问操作系统底层,因为它没有指针。

(1)这种方法调用的过程:

1、在java中申明native方法,然后编译; 
2、用javah产生一个 .h 文件; 
3、写一个 .cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(其中又包含了jdk带的jni.h文件); 
4、将.cpp文件编译成动态链接库文件; 
5、在java中用System.loadLibrary()文件加载第四步产生的动态链接库文件,然后这个navite方法就可被访问了 
Java的API文档对hashCode()方法做了详细的说明,这也是我们重写hashCode()方法时的原则【Object类】;

(2)重点要注意的是:

a. 在java应用程序运行时,无论何时多次调用同一个对象时的hashCode()方法,这个对象的hashCode()方法的返回值必须是相同的一个int值;

b. 如果两个对象equals()返回值为true,则他们的hashCode()也必须返回相同的int值;

c. 如果两个对象equals()返回值为false,则他们的hashCode()返回值也必须不同

现在,我们到这里可以看出,我们重写了equals()方法也要重写hashCode()方法,这是因为要保证上面所述的b,c原则;所以java中的很多类都重写了这两个方法,例如String类,包装类等。

三、在什么情况下需要重写hashCode()方法和equals()方法

当我们自定义的一个类,想要把它的实例保存在集合中时,我们就需要重写这两个方法;集合(Collection)有两个类,一个是List,一个是Set。

List:集合中的元素是有序的,可以重复的;
Set:无序,不可重复的;

(1)以HashSet来举例:

HashSet存放元素时,根据元素的hashCode方法计算出该对象的哈希码,快速找到要存储的位置,然后进行比较,

比较过程如下:

  • 如果该对象哈希码与集合已存在对象的哈希码不一致,则该对象没有与其他对象重复,添加到集合中!
  • 如果存在于该对象相同的哈希码,那么通过equals方法判断两个哈希码相同的对象是否为同一对象(判断的标准是:属性是否相同)
    • 相同对象,不添加。
    • 不同对象,添加。

注意:如果返回值为false,则这个时候会以链表的形式在同一个位置上存放两个元素,这会使得HashSet的性能降低,因为不能快速定位了。示意图如下:

这里写图片描述

还有一种情况就是两个对象的hashCode()返回值不同,但是equals()返回true,这个时候HashSet会把这两个对象都存进去,这就和Set集合不重复的规则相悖了;所以,我们重写了equals()方法时,要按照b,c规则重写hashCode()方法!

四、如何重写这两个方法

如果你决定要重写equals()方法,那么你一定还要明确这么做所带来的风险,并确保自己能写出一个健壮的equals()方法。

一定要注意的一点是,在重写equals()后,一定要重写hashCode()方法。

(1)我们先看看 JavaSE 8 Specification中对equals()方法的说明:

  1. 自反性:A.equals(A)要返回true;
  2. 对称性:如果A.equals(B)返回true, 则B.equals(A)也要返回true;
  3. 传递性:如果A.equals(B)为true, B.equals(C)为true, 则A.equals(C)也要为true. 说白了就是 A = B , B = C , 那么A = C;
  4. 一致性:只要A,B对象的状态没有改变,A.equals(B)必须始终返回true;
  5. A.equals(null) 要返回false;

(2)简单步骤:

为了说明方便,我们先定义一个程序员类(Coder):

class Coder {  
    private String name;  
    private int age;  

    // getters and setters  
} 

我们想要的是,如果2个程序员对象的name和age都是相同的,那么我们就认为这两个程序员是一个人。这时候我们就要重写其equals()方法。因为默认的equals()实际是判断两个引用是否指向内在中的同一个对象,相当于 == 。 

重写时要遵循以下三步:

1、判断是否等于自身:

if(other == this){
   return true;  
}

2、 使用instanceof运算符判断 other 是否为Coder类型的对象:

if(!(other instanceof Coder))  {
   return false;  
}

3、比较Coder类中你自定义的数据域,name和age,一个都不能少:

Coder o = (Coder)other;  
return o.name.equals(name) && o.age == age;

看到这有人可能会问,第3步中有一个强制转换,如果有人将一个Integer类的对象传到了这个equals中,那么会不会扔ClassCastException呢?这个担心其实是多余的.因为我们在第二步中已经进行了instanceof 的判断,如果other是非Coder对象,甚至other是个null, 那么在这一步中都会直接返回false, 从而后面的代码得不到执行的机会。

上面的三步也是<Effective Java>中推荐的步骤,基本可保证万无一失。

我们在大学计算机数据结构课程中都已经学过哈希表(hash table)了,hashCode()方法就是为哈希表服务的。

当我们在使用形如HashMap, HashSet这样前面以Hash开头的集合类时,hashCode()就会被隐式调用以来创建哈希映射关系。

<Effective Java>中给出了一个能最大程度上避免哈希冲突的写法,但我个人认为对于一般的应用来说没有必要搞的这么麻烦.如果你的应用中HashSet中需要存放上万上百万个对象时,那你应该严格遵循书中给定的方法.如果是写一个中小型的应用,那么下面的原则就已经足够使用了:

要保证Coder对象中所有的成员都能在hashCode中得到体现.

@Override  
public int hashCode() {  
    int result = 17;  
    result = result * 31 + name.hashCode();  
    result = result * 31 + age;  

    return result;  
}  

其中int result = 17你也可以改成20, 50等等都可以.看到这里我突然有些好奇,想看一下String类中的hashCode()方法是如何实现的.查文档知:

“Returns a hash code for this string. The hash code for a String object is computed as

s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]

using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates exponentiation. (The hash value of the empty string is zero.)”

对每个字符的ASCII码计算n - 1次方然后再进行加和,可见Sun对hashCode的实现是很严谨的. 这样能最大程度避免2个不同的String会出现相同的hashCode的情况.


参考文章:

1、http://blog.youkuaiyun.com/jing_bufferfly/article/details/50868266 
2、http://blog.youkuaiyun.com/neosmith/article/details/17068365https://blog.youkuaiyun.com/xlgen157387/article/details/63683882

 

<think>好的,我现在需要回答用户的问题:在Java中,重写hashCode方法后是否必须同时重写equals方法?根据用户提供的引用内容,我需要仔细分析这个问题。 首先,用户引用的资料提到,重写equals时必须重写hashCode,这是因为Java的规范要求两者保持一致。比如引用[3]指出,如果两个对象equals返回true,它们的hashCode必须相同。否则在使用哈希集合时会出现问题,比如HashMap可能无法正确找到对象。 那反过来呢?如果只重写hashCode而不重写equals会怎样?假设一个类只重写hashCode,但equals还是使用默认的Object的equals(即比较对象地址)。这时候,即使两个对象逻辑上相等,equals可能返回false,而它们的hashCode相同。这会导致哈希表中存储两个逻辑相等但地址不同的对象,这不符合预期。例如,当用这样的对象作为键时,可能会出现重复的键,导致数据错误。 另外,引用[2][3]都强调,为了保持一致性,必须同时重写两者。所以反过来,如果只重写hashCode而不重写equals,就可能破坏这种一致性。例如,两个对象可能有相同的hashCode(因为重写了),但equals却认为它们不相等,这虽然不违反Java的规范(因为规范允许不同对象有相同的hashCode),但会导致哈希表的性能下降,比如更多的哈希冲突,或者无法正确检索对象。 不过,是否存在必须强制的情况呢?根据Java规范,当你重写equals时,必须同时重写hashCode,但反过来并没有强制要求。也就是说,如果只重写hashCode而不重写equals,Java编译器不会报错。但这样设计是否符合最佳实践呢? 比如,假设某个类需要自定义hashCode的计算方式,但不关心对象的相等性,可能允许不同的实例即使有相同的数据也被视为不同对象。这种情况下,只重写hashCode可能可以接受。但这种情况应该非常少见,因为通常hashCode应该反映对象的相等性。大部分情况下,当需要自定义hashCode时,往往是因为对象的相等性判断也改变了,所以这时候必须同时重写equals。 因此,最佳实践是:当重写hashCode时,通常也应该重写equals,以确保两者的一致性,避免在哈希集合中出现意外行为。虽然不是强制要求,但为了代码的健壮性正确性,同时重写两者是推荐的。引用[3]中提到的一致性原则哈希集合的正确性都支持这一点。所以用户的问题的答案应该是:重写hashCode后不一定必须重写equals,但为了确保一致性正确性,最佳实践是同时重写两者。</think>在 Java 对象设计中,重写 `hashCode` 方法后是否必须同时重写 `equals` 方法?以下是详细分析: ### 1. **重写 `hashCode` 后是否必须重写 `equals`?** - **从语法规范角度**: Java 并未强制要求重写 `hashCode` 时必须重写 `equals` 方法。例如,若某个类的对象仅需自定义哈希值计算方式(如缓存优化),但无需修改对象相等性判断逻辑,则单独重写 `hashCode` 是允许的。 - **从逻辑一致性角度**: 若重写 `hashCode` 的目的是为了适配对象的“逻辑相等性”(例如基于对象属性计算哈希值),则必须同时重写 `equals` 方法。否则,可能出现以下问题: - **逻辑矛盾**:两个对象哈希值相同,但 `equals` 返回 `false`(默认 `Object.equals` 比较对象地址),这会导致哈希表(如 `HashMap`)无法正确识别相等对象[^3]。 - **性能问题**:哈希冲突增加,哈希表退化为链表,查找效率降低。 ### 2. **最佳实践** - **同时重写 `equals` `hashCode`**: 当对象的相等性需要基于属性判断时,必须同时重写这两个方法。例如: ```java public class Student { private String id; private String name; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return Objects.equals(id, student.id); // 基于 id 判断相等性 } @Override public int hashCode() { return Objects.hash(id); // 确保相同 id 的对象哈希值一致 } } ``` - **一致性保障**:若 `id` 相同的两个 `Student` 对象哈希值不同,则它们在 `HashSet` 中会被视为不同元素,导致数据重复[^2]。 - **例外场景**: 若仅需优化哈希值计算(如缓存哈希值),且不依赖属性判断相等性,可单独重写 `hashCode`。但此类场景极少。 ### 3. **违反一致性的后果** - **哈希表行为异常**: 假设仅重写 `hashCode` 未重写 `equals`,两个属性相同的对象可能哈希值相同,但 `equals` 返回 `false`。此时: ```java Map<Student, Integer> map = new HashMap<>(); map.put(new Student("001"), 1); Integer value = map.get(new Student("001")); // 返回 null,因为 equals 比较对象地址 ``` 尽管两个对象哈希值相同,但 `HashMap` 在哈希桶内进一步通过 `equals` 确认键是否匹配。由于 `equals` 未重写,匹配失败,返回 `null`。 ### 4. **总结** - **必须同时重写的情况**:当对象的相等性依赖于属性而非对象地址时。 - **无需重写的情况**:仅需优化哈希计算,且对象相等性仍基于地址(几乎不常见)。 - **最佳实践**:遵循 Java 规范,在修改对象的相等性逻辑时,始终同时重写 `equals` `hashCode`,以确保哈希表行为正确性能优化。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值