为什么我需要重写Java中的equals和hashCode方法?

最近我读了这篇文章 发展商工程文件。< / p >

该文档是关于有效和正确地定义hashCode()equals(),但我不明白为什么我们需要覆盖这两个方法。

我如何决定有效地实现这些方法?

591356 次浏览

因为如果你不重写它们,你将使用Object中的默认实现。

考虑到实例相等和hascode值通常需要了解组成对象的内容,它们通常需要在类中重新定义,以具有任何有形的意义。

假设你有一个类(A),它聚合了另外两个类(B) (C),你需要在哈希表中存储类(A)的实例。默认实现只允许区分实例,但不允许通过(B)和(C)。因此A的两个实例可以相等,但默认不允许您以正确的方式比较它们。

它在使用< >强值对象< / >强时很有用。下面是Portland模式存储库的一个节选:

值对象的例子是事物 比如数字,日期,金钱和 字符串。通常,它们很小 被广泛使用的物体。 他们的身份是基于他们的状态 而不是他们的对象同一性。 通过这种方式,您可以拥有多个副本

所以我可以有多个副本 对象表示日期1月16日 1998. 这些副本中的任何一个都是相等的。对于一个小的 这样的对象,是常有的 更容易创建新的和移动 他们围绕而不是依靠一个

值对象应该总是被覆盖 Java中的.equals()(或Smalltalk中的=)。 (记住重写.hashCode()为 好。)< / p >

你必须覆盖hashCode()在每个 重写equals()的类。失败 这样做会导致违反 总合同 Object.hashCode(),它将防止 你的类不能正常运行 结合所有基于哈希的 集合,包括HashMap, HashSet和Hashtable.


from 有效的Java, by Joshua Bloch

通过一致地定义equals()hashCode(),可以提高类作为基于哈希的集合中的键的可用性。正如hashCode的API文档所解释的那样:“支持此方法是为了使用诸如java.util.Hashtable所提供的哈希表。”

关于如何有效地实现这些方法的问题,最好的答案是建议你阅读有效的Java的第3章。

这两个方法都在Object类中定义。两者都是最简单的实现。所以当你需要你想给这些方法添加更多的实现时你就可以在你的类中重写。

对于对象中的equals()方法只检查它在引用上的相等性。如果你也需要比较它的状态,那么你可以像在String类中那样重写它。

简单地说,Object中的equals-方法检查引用是否相等,当属性相等时,类的两个实例在语义上仍然相等。例如,当将对象放入使用等号和hashcode的容器中时,这很重要,比如HashMap。假设我们有这样一个类:

public class Foo {
String id;
String whatevs;


Foo(String id, String whatevs) {
this.id = id;
this.whatevs = whatevs;
}
}

我们用相同的id创建了两个实例:

Foo a = new Foo("id", "something");
Foo b = new Foo("id", "something else");

如果不重写等号,我们将得到:

  • A.equals (b)为假,因为它们是两个不同的实例
  • a.equals(a)为真,因为它是同一个实例
  • b.equals(b)为真,因为它是同一个实例

正确吗?也许吧,如果这是你想要的。但假设我们希望具有相同id的对象是相同的对象,不管它是否是两个不同的实例。我们重写等号(和hashcode):

public class Foo {
String id;
String whatevs;


Foo(String id, String whatevs) {
this.id = id;
this.whatevs = whatevs;
}


@Override
public boolean equals(Object other) {
if (other instanceof Foo) {
return ((Foo)other).id.equals(this.id);
}
}


@Override
public int hashCode() {
return this.id.hashCode();
}
}

至于实现equals和hashcode,我建议使用番石榴的帮手方法

Joshua Bloch在Effective Java上说

必须在重写equals()的每个类中重写hashCode()。如果不这样做,将违反Object.hashCode()的一般约定,这将阻止您的类与所有基于哈希的集合(包括HashMap、HashSet和Hashtable)一起正常运行。

让我们试着用一个例子来理解它,如果我们覆盖equals()而不覆盖hashCode(),并试图使用Map,会发生什么。

假设我们有这样一个类,并且MyClass的两个对象如果它们的importantField相等(由eclipse生成hashCode()equals())则相等

public class MyClass {
private final String importantField;
private final String anotherField;


public MyClass(final String equalField, final String anotherField) {
this.importantField = equalField;
this.anotherField = anotherField;
}


@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((importantField == null) ? 0 : importantField.hashCode());
return result;
}


@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final MyClass other = (MyClass) obj;
if (importantField == null) {
if (other.importantField != null)
return false;
} else if (!importantField.equals(other.importantField))
return false;
return true;
}
}

想象一下你有这个

MyClass first = new MyClass("a","first");
MyClass second = new MyClass("a","second");

只覆盖equals

如果只有equals被覆盖,那么当你首先调用myMap.put(first,someValue)时,它将哈希到某个bucket,当你调用myMap.put(second,someOtherValue)时,它将哈希到其他某个bucket(因为它们有不同的hashCode)。所以,尽管它们是相等的,因为它们不散列到同一个桶,映射无法意识到这一点,它们都留在映射中。


虽然如果我们重写hashCode(),就没有必要重写equals(),但让我们看看在这种特殊情况下会发生什么,我们知道MyClass的两个对象是相等的,如果它们的importantField相等,但我们没有重写equals()

只覆盖hashCode

如果你只覆盖hashCode,那么当你调用myMap.put(first,someValue)时,它首先接受,计算它的hashCode并将其存储在给定的存储桶中。然后,当你调用myMap.put(second,someOtherValue)时,它应该根据映射文档将first替换为second,因为它们是相等的(根据业务需求)。

但问题是,equals没有被重新定义,所以当映射哈希second并遍历桶时,寻找是否有一个对象k,以便second.equals(k)为真,它将找不到任何对象,因为second.equals(first)将为false

希望这是清楚的

我正在研究解释“如果你只覆盖hashCode,那么当你调用myMap.put(first,someValue)时,它首先接受,计算它的hashCode并将其存储在给定的bucket中。然后,当你调用myMap.put(first,someOtherValue)时,它应该根据地图文档将first替换为second,因为它们是相等的(根据我们的定义)。”:

我认为第二次添加myMap时,它应该是像myMap.put(second,someOtherValue)这样的“第二个”对象

equals和hashcode方法在对象类中定义。默认情况下,如果equals方法返回true,则系统将进一步检查哈希码的值。如果这两个对象的哈希码也相同,则认为这两个对象是相同的。因此,如果只重写equals方法,那么即使重写的equals方法指示两个对象相等,系统定义的hashcode可能不会指示两个对象相等。所以我们也需要重写哈希码。

hashCode():

如果你只重写哈希码方法,什么也不会发生,因为它总是为每个对象返回一个新的hashCode作为object类。

equals():

如果你只重写equals方法,如果a.equals(b)为真,这意味着a和b的hashCode必须相同,但这不会发生,因为你没有重写hashCode方法。

注意:Object类的hashCode()方法总是为每个对象返回一个新的hashCode

因此,当你需要在基于哈希的集合中使用你的对象时,你必须重写equals()hashCode()

Java提出了一个规则

如果使用Object类的equals方法,两个对象相等,那么hashcode方法应该为这两个对象提供相同的值。

因此,如果在我们的类中重写equals(),我们也应该重写hashcode()方法以遵循此规则。 例如,equals()hashcode()这两个方法都在Hashtable中使用,以键值对的形式存储值。如果我们覆盖其中一个对象而不覆盖另一个对象,如果我们使用这样的对象作为键,Hashtable可能不能像我们想要的那样工作

为了在HashMap, Hashtable等集合中使用我们自己的类对象作为键。,我们应该通过了解集合的内部工作来重写这两个方法(hashCode()和equals())。否则,它会导致我们意想不到的错误结果。

hashCode()方法用于获取给定对象的唯一整数。当该对象需要存储在一些HashTableHashMap之类的数据结构中时,此整数用于确定桶的位置。默认情况下,Object的hashCode()方法返回对象存储的内存地址的整数表示形式。

对象的hashCode()方法用于将对象插入到HashTableHashMapHashSet中。关于HashTables的更多信息请参见Wikipedia.org。

要在map数据结构中插入任何条目,我们需要键和值。如果键和值都是用户定义的数据类型,则键的hashCode()将确定在内部存储对象的位置。当还需要从映射中查找对象时,键的哈希码将确定在哪里搜索对象。

哈希码只在内部指向某个“区域”(或列表,桶等)。因为不同的键对象可能具有相同的哈希码,所以哈希码本身并不能保证找到正确的键。然后HashTable迭代该区域(所有键都具有相同的哈希码),并使用键的equals()方法找到正确的键。一旦找到正确的键,就会返回为该键存储的对象。

因此,正如我们所看到的,在HashTable中存储和查找对象时使用了hashCode()equals()方法的组合。

注:

  1. 总是使用对象的相同属性来生成hashCode()equals()。在我们的例子中,我们使用了员工id。

  2. equals()必须是一致的(如果对象没有被修改,那么它必须一直返回相同的值)。

  3. 无论何时a.equals(b),则a.hashCode()必须与b.hashCode()相同。

  4. 如果你覆盖了一个,那么你也应该覆盖另一个。

http://parameshk.blogspot.in/2014/10/examples-of-comparable-comporator.html

HashMapHashSet这样的集合使用对象的hashcode值来确定它应该如何存储在集合中,并且再次使用hashcode来定位对象

哈希检索是一个两步过程:

  1. 找到正确的桶(使用hashCode())
  2. 在桶中搜索正确的元素(使用equals())

下面是一个关于为什么我们应该覆盖equals()hashcode()的小例子。

考虑一个Employee类,它有两个字段:年龄和名字。

public class Employee {


String name;
int age;


public Employee(String name, int age) {
this.name = name;
this.age = age;
}


public String getName() {
return name;
}


public void setName(String name) {
this.name = name;
}


public int getAge() {
return age;
}


public void setAge(int age) {
this.age = age;
}


@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Employee))
return false;
Employee employee = (Employee) obj;
return employee.getAge() == this.getAge()
&& employee.getName() == this.getName();
}


// commented
/*  @Override
public int hashCode() {
int result=17;
result=31*result+age;
result=31*result+(name!=null ? name.hashCode():0);
return result;
}
*/
}

现在创建一个类,将Employee对象插入到HashSet中,并测试该对象是否存在。

public class ClientTest {
public static void main(String[] args) {
Employee employee = new Employee("rajeev", 24);
Employee employee1 = new Employee("rajeev", 25);
Employee employee2 = new Employee("rajeev", 24);


HashSet<Employee> employees = new HashSet<Employee>();
employees.add(employee);
System.out.println(employees.contains(employee2));
System.out.println("employee.hashCode():  " + employee.hashCode()
+ "  employee2.hashCode():" + employee2.hashCode());
}
}

它将打印以下内容:

false
employee.hashCode():  321755204  employee2.hashCode():375890482

现在取消hashcode()方法的注释,执行相同的方法,输出将是:

true
employee.hashCode():  -938387308  employee2.hashCode():-938387308
现在你明白为什么如果两个对象被认为是相等的,它们的__abc0必须 也相等吗?否则,您将永远无法找到默认的对象 Object类中的hashcode方法实际上总是得到一个唯一的数字 对于每个对象,即使equals()方法被以这样一种方式覆盖 或者更多的对象被认为是相等的。对象是否相等并不重要,如果 它们的__abc没有反映这一点。再说一遍,如果两个对象相等,他们的 Hashcode s也必须相等。

加上@Lombo的答案

什么时候你需要重写equals()

Object的equals()的默认实现是

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

,这意味着只有当两个对象具有相同的内存地址时才会被认为是相等的,只有当你是时才会为真

.比较对象本身 但是如果一个对象的值相同,你可能会认为两个对象是相同的 或更多的属性(参考@Lombo的回答中给出的例子)。< / p >

因此,在这些情况下,你将重写equals(),并给出你自己的相等条件。

我已经成功地实现了equals(),它工作得很好。那么为什么他们要求重写hashCode()呢?< / em >

< p >。只要不在用户定义的类上使用基于“哈希”的集合,就没问题。 但是在将来的某个时候,你可能想要使用HashMapHashSet,如果你不使用overridehashCode(),这些基于哈希的收集将无法正常工作

覆盖只等于(除了@Lombo的答案)

myMap.put(first,someValue)
myMap.contains(second); --> But it should be the same since the key are the same.But returns false!!! How?
首先,HashMap检查second的hashCode是否与first相同。 只有当值相同时,才会继续检查同一桶中的相等性 但是这里这两个对象的hashCode是不同的(因为它们有不同的内存地址-从默认实现)。 因此,它甚至不会关心是否相等 如果你在重写的equals()方法中有一个断点,如果它们有不同的hashcode,它就不会介入。 contains()检查hashCode(),只有当它们相同时才会调用你的equals()方法

为什么我们不能让HashMap在所有桶中检查相等?所以我没有必要重写hashCode() !!< / em >

那么你就错过了基于哈希集合的要点。

Your hashCode() implementation : intObject%9.

以下是以桶的形式存储的密钥。

Bucket 1 : 1,10,19,... (in thousands)
Bucket 2 : 2,20,29...
Bucket 3 : 3,21,30,...
...
例如,您想知道映射是否包含键10。 你想把所有的桶都搜一遍吗?或“是否只搜索一个桶?”< / p > 根据hashCode,您将确定如果10存在,它必须存在于Bucket 1中。 所以只有桶1会被搜索!!< / p >

让我用非常简单的话来解释这个概念。

首先,从更广泛的角度来看,我们有集合,而hashmap是集合中的数据结构之一。

要理解为什么我们必须重写equals和hashcode方法,如果需要的话,首先要理解什么是hashmap以及它的功能。

hashmap是一种以数组方式存储键值对数据的数据结构。假设是a[],其中'a'中的每个元素都是一个键值对。

此外,上述数组中的每个索引都可以是链表,因此在一个索引上有多个值。

为什么要使用hashmap呢?

如果我们必须在一个大数组中搜索,那么搜索每个数组,如果它们不是有效的,那么哈希技术告诉我们,让我们用一些逻辑预处理数组,并根据该逻辑对元素进行分组,即哈希

例如:我们有数组1、2、3、4、5、6、7、8、9、10、11,我们应用哈希函数mod 10,所以1、11将被分组在一起。因此,如果我们必须在前一个数组中搜索11,那么我们必须迭代整个数组,但当我们对它进行分组时,我们限制了迭代的范围,从而提高了速度。为了简单起见,用于存储所有上述信息的数据结构可以看作是一个2d数组

现在除了上面的hashmap还告诉它不会在其中添加任何duplicate。这就是为什么我们要重写等号和hashcode的主要原因

因此,当我们说要解释hashmap的内部工作时,我们需要找到hashmap有什么方法,以及它如何遵循上面我解释过的规则

所以hashmap有一个方法叫as put(K,V),根据hashmap,它应该遵循上面的规则,有效地分配数组,不添加任何重复

put所做的是首先为给定的键生成hashcode来决定值应该放在哪个索引中。如果那个下标处什么都没有,那么新值就会被加到那里,如果那里已经有了,那么新值就会被加到链表末尾那个下标处。但是请记住,不应该根据期望的hashmap行为添加重复项。假设你有两个整数对象aa=11 bb=11。

由于每个对象都派生自对象类,比较两个对象的默认实现是比较引用,而不是对象内部的值。因此,在上述情况下,尽管语义上相同,但两个对象都将无法通过相等性测试,并且有可能存在两个具有相同hashcode和相同值的对象,从而创建重复的对象。如果我们重写,就可以避免添加重复项。 你也可以引用详细的工作

import java.util.HashMap;




public class Employee {
String name;
String mobile;


public Employee(String name,String mobile) {
this.name = name;
this.mobile = mobile;
}
    

@Override
public int hashCode() {
System.out.println("calling hascode method of Employee");
String str = this.name;
int sum = 0;
for (int i = 0; i < str.length(); i++) {
sum = sum + str.charAt(i);
}
return sum;
}


@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
System.out.println("calling equals method of Employee");
Employee emp = (Employee) obj;
if (this.mobile.equalsIgnoreCase(emp.mobile)) {
System.out.println("returning true");
return true;
} else {
System.out.println("returning false");
return false;
}
}


public static void main(String[] args) {
// TODO Auto-generated method stub


Employee emp = new Employee("abc", "hhh");
Employee emp2 = new Employee("abc", "hhh");
HashMap<Employee, Employee> h = new HashMap<>();
//for (int i = 0; i < 5; i++) {
h.put(emp, emp);
h.put(emp2, emp2);
//}
        

System.out.println("----------------");
System.out.println("size of hashmap: "+h.size());
}
}

考虑在一个桶中收集所有黑色的球。你的工作是像下面这样给这些球上色,并将其用于适当的游戏,

为网球-黄色,红色。 对于Cricket - White

现在水桶有三种颜色的球黄色,红色和白色。现在只有你自己知道哪个颜色适合哪个游戏。

着色球-哈希。 选择比赛用的球-等于。

如果你给球上色,然后有人选了板球或网球,他们不会介意颜色的!!

class A {
int i;
// Hashing Algorithm
if even number return 0 else return 1
// Equals Algorithm,
if i = this.i return true else false
}
  • put('key','value')将使用hashCode()计算哈希值来确定 桶,并使用equals()方法来查找该值是否已经 出现在桶里。否则将被替换为当前值
  • get('key')将使用hashCode()首先找到条目(桶) equals()来查找条目
  • 中的值

如果两者都被覆盖,

Map< 一个 >

Map.Entry 1 --> 1,3,5,...
Map.Entry 2 --> 2,4,6,...

If =没有被覆盖

Map< 一个 >

Map.Entry 1 --> 1,3,5,...,1,3,5,... // Duplicate values as equals not overridden
Map.Entry 2 --> 2,4,6,...,2,4,..

如果hashCode没有被覆盖

Map< 一个 >

Map.Entry 1 --> 1
Map.Entry 2 --> 2
Map.Entry 3 --> 3
Map.Entry 4 --> 1
Map.Entry 5 --> 2
Map.Entry 6 --> 3 // Same values are Stored in different hasCodes violates Contract 1
So on...

HashCode等价契约

  1. 根据equal方法,两个相等的键应该生成相同的hashCode
  2. 生成相同hashCode的两个key不需要相等(在上面的例子中,所有偶数生成相同的hashCode)

身份不是平等。

  • =操作符==测试标识符。
  • equals(Object obj)方法比较相等性检验(即我们需要通过重写方法来告诉相等)

为什么我需要重写Java中的equals和hashCode方法?

首先我们要理解等号法的用法。

为了识别两个对象之间的差异,我们需要重写equals方法。

例如:

Customer customer1=new Customer("peter");
Customer customer2=customer1;
customer1.equals(customer2); // returns true by JVM. i.e. both are refering same Object
------------------------------
Customer customer1=new Customer("peter");
Customer customer2=new Customer("peter");
customer1.equals(customer2); //return false by JVM i.e. we have two different peter customers.


------------------------------
Now I have overriden Customer class equals method as follows:
@Override
public boolean equals(Object obj) {
if (this == obj)   // it checks references
return true;
if (obj == null) // checks null
return false;
if (getClass() != obj.getClass()) // both object are instances of same class or not
return false;
Customer other = (Customer) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name)) // it again using bulit in String object equals to identify the difference
return false;
return true;
}
Customer customer1=new Customer("peter");
Customer customer2=new Customer("peter");
Insteady identify the Object equality by JVM, we can do it by overring equals method.
customer1.equals(customer2);  // returns true by our own logic

现在hashCode方法很容易理解了。

hashCode生成整数,以便将对象存储在HashMapHashSet等数据结构中。

假设我们有如上所述的Customer的override equals方法,

customer1.equals(customer2);  // returns true by our own logic

在处理数据结构时,我们将对象存储在桶中(桶是文件夹的花哨名称)。如果我们使用内置哈希技术,对于以上两个客户,它会生成两个不同的哈希码。所以我们把相同的对象存储在两个不同的地方。为了避免这类问题,我们也应该基于以下原则重写hashCode方法。

  • 不相等的实例可能具有相同的hashcode。
  • 相同的实例应该返回相同的hashcode。

Bah -“你必须在每个重写equals()的类中重写hashCode()。”

[出自Joshua Bloch的《Effective Java》?]

这不是反了吗?重写hashCode可能意味着您正在编写一个哈希键类,但重写equals肯定不会。有许多类没有用作散列键,但由于其他原因确实需要逻辑相等性测试方法。如果您为它选择“等于”,那么您可能会被要求通过过度应用此规则来编写hashCode实现。这样做所实现的只是在代码库中添加未经测试的代码,这是一个等待在未来绊倒某人的恶魔。编写你不需要的代码也是反敏捷的。这是错误的(并且ide生成的函数可能与您手工制作的函数不兼容)。

他们肯定应该在被写来用作键的对象上强制设置一个接口吗?无论如何,Object永远不应该提供默认的hashCode()和equals() imho。它可能鼓励了许多破碎的散列集合。

但无论如何,我认为“规则”是前后颠倒的。与此同时,我将继续避免使用“等号”进行相等性测试方法:-(

在下面的例子中,如果您注释掉Person类中equals或hashcode的覆盖,此代码将无法查找Tom的订单。使用哈希码的默认实现可能会导致哈希表查找失败。

下面是一个简化的代码,它按Person提取人们的订单。Person被用作哈希表中的键。

public class Person {
String name;
int age;
String socialSecurityNumber;


public Person(String name, int age, String socialSecurityNumber) {
this.name = name;
this.age = age;
this.socialSecurityNumber = socialSecurityNumber;
}


@Override
public boolean equals(Object p) {
//Person is same if social security number is same


if ((p instanceof Person) && this.socialSecurityNumber.equals(((Person) p).socialSecurityNumber)) {
return true;
} else {
return false;
}


}


@Override
public int hashCode() {        //I am using a hashing function in String.java instead of writing my own.
return socialSecurityNumber.hashCode();
}
}




public class Order {
String[]  items;


public void insertOrder(String[]  items)
{
this.items=items;
}


}






import java.util.Hashtable;


public class Main {


public static void main(String[] args) {


Person p1=new Person("Tom",32,"548-56-4412");
Person p2=new Person("Jerry",60,"456-74-4125");
Person p3=new Person("Sherry",38,"418-55-1235");


Order order1=new Order();
order1.insertOrder(new String[]{"mouse","car charger"});


Order order2=new Order();
order2.insertOrder(new String[]{"Multi vitamin"});


Order order3=new Order();
order3.insertOrder(new String[]{"handbag", "iPod"});


Hashtable<Person,Order> hashtable=new Hashtable<Person,Order>();
hashtable.put(p1,order1);
hashtable.put(p2,order2);
hashtable.put(p3,order3);


//The line below will fail if Person class does not override hashCode()
Order tomOrder= hashtable.get(new Person("Tom", 32, "548-56-4412"));
for(String item:tomOrder.items)
{
System.out.println(item);
}
}
}

Java中的Equals和Hashcode方法

它们是java.lang. object类的方法,object类是所有类(自定义类以及java API中定义的其他类)的超类。

实现:

public boolean equals(对象obj)

hashCode()

enter image description here

public boolean equals(对象obj)

这个方法只是检查两个对象引用x和y是否引用同一个对象。例如,它检查x是否== y。

它是反射性的:对于任何参考值x, x = (x)应该返回true。

是对称的:对于任何参考值x和y,当且仅当y = (x)返回true时,x = (y)应该返回true。

它是可传递的:对于任何参考值x、y和z,如果x.x = (y)返回true,而y.y = (z)返回true,则x.x = (z)应该返回true。

它是一致的:对于任何参考值x和y, x.equals(y)的多次调用一致返回true或一致返回false,前提是对象上的等号比较中使用的信息没有被修改。

对于任何非空参考值x, x = (null)应该返回 假的。< / p >

hashCode()

此方法返回调用此方法的对象的哈希码值。此方法以整数形式返回哈希码值,支持基于哈希的集合类,如Hashtable、HashMap、HashSet等。必须在重写equals方法的每个类中重写此方法。

hashCode的一般契约是:

在Java应用程序的执行过程中,只要在同一个对象上多次调用hashCode方法,hashCode方法必须一致地返回相同的整数,前提是该对象上的等号比较中使用的信息没有被修改。

这个整数不需要在应用程序的一次执行和同一应用程序的另一次执行之间保持一致。

如果根据equals(Object)方法,两个对象相等,那么在这两个对象上调用hashCode方法必须产生相同的整数结果。

如果根据equals(java.lang.Object)方法,两个对象是不相等的,那么对每个对象调用hashCode方法必须产生不同的整数结果,这是不要求的。然而,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

相等的对象必须产生相同的哈希码,只要它们是 相等的,但是不相等的对象不需要产生不同的哈希码

资源:

JavaRanch

图片 .

恕我冒昧,这是根据规则说的-如果两个对象相等,那么它们应该具有相同的哈希值,即相等的对象应该产生相同的哈希值。

如上所述,Object is ==中的默认equals()对地址进行比较,hashCode()返回整数形式的地址(对实际地址进行哈希),这对于不同的Object来说也是不同的。

如果你需要在基于哈希的集合中使用自定义对象,你需要覆盖equals()和hashCode(),例如如果我想维护员工对象的HashSet,如果我不使用更强的hashCode和equals,我可能最终会覆盖两个不同的员工对象,这发生在我使用年龄作为hashCode()时,但是我应该使用唯一的值,可以是员工ID。

1) 常见错误如下例所示。

public class Car {


private String color;


public Car(String color) {
this.color = color;
}


public boolean equals(Object obj) {
if(obj==null) return false;
if (!(obj instanceof Car))
return false;
if (obj == this)
return true;
return this.color.equals(((Car) obj).color);
}


public static void main(String[] args) {
Car a1 = new Car("green");
Car a2 = new Car("red");


//hashMap stores Car type and its quantity
HashMap<Car, Integer> m = new HashMap<Car, Integer>();
m.put(a1, 10);
m.put(a2, 20);
System.out.println(m.get(new Car("green")));
}
}

绿色的车没有找到

2. hashCode()引起的问题

该问题是由未覆盖的方法hashCode()引起的。equals()hashCode()之间的契约是:

  1. 如果两个对象相等,那么它们必须具有相同的哈希码。
  2. 如果两个对象具有相同的哈希码,它们可能相等,也可能不相等。

    public int hashCode(){
    return this.color.hashCode();
    }
    

String类和包装器类的equals()hashCode()方法的实现与Object类不同。Object类的equals()方法比较对象的引用,而不是内容。Object类的hashCode()方法为每个对象返回不同的hashCode,无论内容是否相同。

当你使用Map集合并且键是Persistent类型,StringBuffer/builder类型时,它会导致问题。因为它们不像String类那样重写equals()和hashCode(),所以equals()在比较两个不同的对象时将返回false,即使它们具有相同的内容。它将使hashMap存储相同的内容键。存储相同的内容键意味着它违反了Map的规则,因为Map根本不允许重复键。 因此,在类中重写equals()和hashCode()方法,并提供实现(IDE可以生成这些方法),以便它们与String的equals()和hashCode()工作相同,并防止相同的内容键。< / p >

你必须重写hashCode()方法和equals(),因为equals()根据hashCode工作。

此外,与equals()一起重写hashCode()方法有助于完好无损equals()-hashCode()契约:“如果两个对象相等,那么它们必须具有相同的哈希码。”

什么时候需要为hashCode()编写自定义实现?

我们知道,HashMap的内部工作是基于哈希(Hashing)原则的。条目集存储在特定的存储桶中。您可以根据自己的需求定制hashCode()实现,以便相同的类别对象可以存储在相同的索引中。 当你使用__abc0方法将值存储到Map集合时,put()的内部实现是:

put(k, v){
hash(k);
index=hash & (n-1);
}

意思是,它生成索引,索引是基于特定键对象的hashcode生成的。所以让这个方法根据你的需求生成哈希码,因为相同的哈希码条目集将存储在相同的桶或索引中。

就是这样!

为了帮助你检查重复的对象,我们需要一个自定义的等号和hashCode。

由于hashcode总是返回一个数字,它总是快速检索使用数字而不是字母键的对象。它会怎么样?假设我们通过传递一个已经在其他对象中可用的值来创建一个新对象。现在,新对象将返回与另一个对象相同的哈希值,因为传递的值是相同的。一旦返回相同的哈希值,JVM将每次访问相同的内存地址,如果有多个对象存在相同的哈希值,它将使用equals()方法来识别正确的对象。

当你想在Map中作为键存储和检索自定义对象时,你应该总是覆盖自定义对象中的equals和hashCode。 如:< / p >
Person p1 = new Person("A",23);
Person p2 = new Person("A",23);
HashMap map = new HashMap();
map.put(p1,"value 1");
map.put(p2,"value 2");

这里p1 &p2将只被认为是一个对象,map的大小将只有1,因为它们是相等的。

public class Employee {


private int empId;
private String empName;


public Employee(int empId, String empName) {
super();
this.empId = empId;
this.empName = empName;
}


public int getEmpId() {
return empId;
}


public void setEmpId(int empId) {
this.empId = empId;
}


public String getEmpName() {
return empName;
}


public void setEmpName(String empName) {
this.empName = empName;
}


@Override
public String toString() {
return "Employee [empId=" + empId + ", empName=" + empName + "]";
}


@Override
public int hashCode() {
return empId + empName.hashCode();
}


@Override
public boolean equals(Object obj) {


if (this == obj) {
return true;
}
if (!(this instanceof Employee)) {
return false;
}
Employee emp = (Employee) obj;
return this.getEmpId() == emp.getEmpId() && this.getEmpName().equals(emp.getEmpName());
}


}

测试类

public class Test {


public static void main(String[] args) {
Employee emp1 = new Employee(101,"Manash");
Employee emp2 = new Employee(101,"Manash");
Employee emp3 = new Employee(103,"Ranjan");
System.out.println(emp1.hashCode());
System.out.println(emp2.hashCode());
System.out.println(emp1.equals(emp2));
System.out.println(emp1.equals(emp3));
}


}

在对象类中,equals(Object obj)用于比较地址比较,这就是为什么在Test类中,如果你比较两个对象,则equals method给出false,但当我们重写hashcode()时,它可以比较内容并给出正确的结果。

如果你重写了equals()而不是hashcode(),你不会发现任何问题,除非你或其他人在诸如HashSet这样的散列集合中使用了该类类型。 在我之前的人已经清楚地解释了很多次文献理论,我只是在这里提供一个非常简单的例子

考虑一个类,它的equals()需要表示自定义的东西:-

    public class Rishav {


private String rshv;


public Rishav(String rshv) {
this.rshv = rshv;
}


/**
* @return the rshv
*/
public String getRshv() {
return rshv;
}


/**
* @param rshv the rshv to set
*/
public void setRshv(String rshv) {
this.rshv = rshv;
}


@Override
public boolean equals(Object obj) {
if (obj instanceof Rishav) {
obj = (Rishav) obj;
if (this.rshv.equals(((Rishav) obj).getRshv())) {
return true;
} else {
return false;
}
} else {
return false;
}
}


@Override
public int hashCode() {
return rshv.hashCode();
}


}

现在考虑这个主类:-

    import java.util.HashSet;
import java.util.Set;


public class TestRishav {


public static void main(String[] args) {
Rishav rA = new Rishav("rishav");
Rishav rB = new Rishav("rishav");
System.out.println(rA.equals(rB));
System.out.println("-----------------------------------");


Set<Rishav> hashed = new HashSet<>();
hashed.add(rA);
System.out.println(hashed.contains(rB));
System.out.println("-----------------------------------");


hashed.add(rB);
System.out.println(hashed.size());
}


}

这将产生以下输出:-

    true
-----------------------------------
true
-----------------------------------
1

我对结果很满意。但是如果我没有覆盖hashCode(),它将导致噩梦,因为具有相同成员内容的Rishav对象将不再被视为唯一的,因为hashCode将是不同的,因为由默认行为生成,下面是将被输出:-

    true
-----------------------------------
false
-----------------------------------
2

在这个回答中没有提到测试equals/hashcode契约。

我发现EqualsVerifier库非常有用和全面。它也很容易使用。

此外,从头构建equals()hashCode()方法涉及大量样板代码。Apache Commons Lang库提供了EqualsBuilderHashCodeBuilder类。这些类极大地简化了复杂类的equals()hashCode()方法的实现。

顺便说一句,值得考虑重写toString()方法来帮助调试。Apache Commons Lang库提供了ToStringBuilder类来帮助实现这一点。

为什么要重写equals()方法

在Java中,我们不能重载==、+=、-+等操作符的行为。他们的行为是特定的。让我们关注一下这里的运算符==。

operator ==如何工作。

它检查我们比较的两个引用是否指向内存中的同一个实例。只有当这两个引用表示内存中的同一个实例时,运算符==才会解析为true。

现在让我们考虑下面的例子

public class Person {


private Integer age;
private String name;
    

..getters, setters, constructors
}

假设在你的程序中,你在不同的地方建立了2个Person对象,你希望比较它们。

Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 );  --> will print false!

这两个对象从商业角度看是一样的,对吧?对于JVM,它们是不一样的。因为它们都是用new关键字创建的,所以这些实例位于内存中的不同段中。因此运算符==将返回

但是如果我们不能重写==操作符,我们怎么能对JVM说我们希望这两个对象被视为相同的。这里出现了.equals()方法。

您可以重写equals()来检查某些对象是否具有相同的值,以便将特定字段视为相等。

您可以选择要比较的字段。如果我们说2个Person对象当且仅当它们具有相同的年龄和相同的名称时是相同的,那么IDE将为自动生成equals()创建如下内容

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}

让我们回到前面的例子

    Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1 == person2 );   --> will print false!
System.out.println ( person1.equals(person2) );  --> will print true!

所以我们不能重载==运算符以我们想要的方式来比较对象,但是Java给了我们另一种方法,equals()方法,我们可以随心所欲地重写它。

记住然而,如果我们不提供自定义版本的.equals()(又名重写)在我们的类中,那么预定义的.equals()来自Object类和==操作符将表现完全相同。

默认的equals()方法继承自Object,它将检查两个比较实例在内存中是否相同!

为什么要重写hashCode()方法

java中的一些数据结构(如HashSet, HashMap)基于应用于这些元素上的哈希函数来存储它们的元素。哈希函数是hashCode()

如果我们可以选择重写.equals()方法,那么我们也必须选择重写hashCode()方法。这是有原因的。

继承自Object的hashCode()的默认实现认为内存中的所有对象都是唯一的!

让我们回到哈希数据结构。对于这些数据结构有一个规则。

HashSet不能包含重复的值,HashMap不能包含重复的键

HashSet是在幕后使用HashMap实现的,HashSet的每个值都存储为HashMap中的一个键。

所以我们必须理解HashMap是如何工作的。

简单地说,HashMap是一个具有一些桶的原生数组。每个桶都有一个linkedList。在那个linkedList中存储了我们的键。HashMap通过应用hashCode()方法为每个键定位正确的linkedList,之后它遍历该linkedList中的所有元素,并对每个元素应用equals()方法来检查该元素是否已经包含在其中。不允许重复密钥。

enter image description here

当我们在HashMap中放东西时,键就存储在其中一个linkedlist中。该键将存储在哪个linkedList中,由该键上的hashCode()方法的结果显示。因此,如果key1.hashCode()的结果是4,那么key1将存储在数组的第4个桶中,在那里存在的linkedList中。

默认情况下,hashCode()方法为每个不同的实例返回不同的结果。如果我们有默认的equals(),它的行为类似于==,将内存中的所有实例视为不同的对象,我们就没有任何问题。

但在前面的示例中,我们说过,如果Person实例的年龄和名字匹配,则认为Person实例是相等的。

    Person person1 = new Person("Mike", 34);
Person person2 = new Person("Mike", 34);
System.out.println ( person1.equals(person2) );  --> will print true!

现在让我们创建一个映射,将这些实例存储为键,并使用一些字符串作为pair值

Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");

在Person类中,我们没有重写hashCode方法,但我们已经重写了equals方法。由于默认的hashCode为不同的java实例提供不同的结果,person1.hashCode()person2.hashCode()有很大的机会得到不同的结果。

我们的映射可能以这些人在不同的链表中结束。

enter image description here

这违背了HashMap的逻辑

一个HashMap不允许有多个相等的键!

但我们现在有了,原因是从对象类继承的默认hashCode()是不够的。在Person类上重写equals()方法之后就不会了。

这就是为什么在重写equals方法之后必须重写hashCode()方法的原因。

现在我们来解决这个问题。让我们重写hashCode()方法来考虑equals()所考虑的相同字段,即age, name

 public class Person {


private Integer age;
private String name;
    

..getters, setters, constructors


@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
name.equals(person.name);
}


@Override
public int hashCode() {
return Objects.hash(name, age);
}


}

现在让我们再次尝试将这些键保存到HashMap中

Map<Person, String> map = new HashMap();
map.put(person1, "1");
map.put(person2, "2");

person1.hashCode()person2.hashCode()肯定是相同的。假设它是0。

HashMap会转到bucket 0, LinkedList会将person1保存为值为“;1”的键。对于第二次放置HashMap是足够智能的,当它再次进入0桶时保存person2键值“;2”;它会看到另一个相等的键已经存在。它会覆盖之前的键。最后HashMap中只有person2键。

enter image description here

现在我们与哈希映射规则保持一致,该规则说不允许有多个相等的键!