最近我读了这篇文章 开发人员工作文件。

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

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


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

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


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


它在使用值对象时很有用。以下摘自Portland Pattern Repository:

Examples of value objects are things like numbers, dates, monies and strings. Usually, they are small objects which are used quite widely. Their identity is based on their state rather than on their object identity. This way, you can have multiple copies of the same conceptual value object. So I can have multiple copies of an object that represents the date 16 Jan 1998. Any of these copies will be equal to each other. For a small object such as this, it is often easier to create new ones and move them around rather than rely on a single object to represent the date. A value object should always override .equals() in Java (or = in Smalltalk). (Remember to override .hashCode() as well.)


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

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

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


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

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


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

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,我建议使用Guava的helper方法


Joshua Bloch在Effective Java上说

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

让我们通过一个例子来理解它,如果我们重写equals()而不重写hashCode()并尝试使用Map会发生什么。

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

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


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

只覆盖hashCode

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

但问题是,equals没有被重新定义,所以当map哈希second并遍历bucket时,寻找是否有一个对象k,使得second.equals(k)为真,它不会找到任何对象,因为second.equals(first)将为假。

希望这是清楚的


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

我认为第二次添加myMap时应该是第二个对象比如myMap。put(second,someOtherValue)


The methods equals and hashcode are defined in the object class. By default if the equals method returns true, then the system will go further and check the value of the hash code. If the hash code of the 2 objects is also same only then the objects will be considered as same. So if you override only equals method, then even though the overridden equals method indicates 2 objects to be equal , the system defined hashcode may not indicate that the 2 objects are equal. So we need to override hash code as well.


hashCode ():

如果只重写hash-code方法,什么也不会发生,因为它总是为每个对象返回一个新的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中以键值对的形式存储值。如果我们覆盖其中一个而不是另一个,如果我们使用这样的对象作为键,哈希表可能不会像我们想要的那样工作。


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


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

对象的hashCode()方法用于将对象插入到HashTable、HashMap或HashSet中。更多关于哈希表的信息请参见Wikipedia.org。

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

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

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

注:

总是使用对象的相同属性来生成hashCode()和equals()。在我们的例子中,我们使用了员工id。 Equals()必须是一致的(如果对象没有被修改,那么它必须一直返回相同的值)。 当a. = (b)时,则a.hashCode()必须与b. hashcode()相同。 如果你覆盖了一个,那么你也应该覆盖另一个。

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


HashMap和HashSet等集合使用对象的hashcode值来确定该对象应该如何存储在集合中,然后再次使用hashcode来定位该对象 在它的收藏中。

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

找到正确的桶(使用hashCode()) 在桶中搜索正确的元素(使用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

现在uncomment hashcode()方法,执行相同的方法,输出将是:

true
employee.hashCode():  -938387308  employee2.hashCode():-938387308

Now can you see why if two objects are considered equal, their hashcodes must also be equal? Otherwise, you'd never be able to find the object since the default hashcode method in class Object virtually always comes up with a unique number for each object, even if the equals() method is overridden in such a way that two or more objects are considered equal. It doesn't matter how equal the objects are if their hashcodes don't reflect that. So one more time: If two objects are equal, their hashcodes must be equal as well.


加上@Lombo的答案

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

Object的equals()的默认实现是

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

这意味着两个对象将被认为是相等的,只有当他们有相同的内存地址,这将是真的,只有当你是 比较对象本身。

但是,如果两个对象对一个对象具有相同的值,则可能认为它们是相同的 或更多的属性(参考@Lombo的回答中给出的例子)。

所以在这些情况下,你会重写equals()你会给出你自己的相等条件。

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

好。只要不在用户定义的类上使用基于“Hash”的集合,就没问题。 但是在将来的某个时候,你可能想要使用HashMap或HashSet,如果你没有覆盖和“正确实现”hashCode(),这些基于Hash的集合将无法正常工作。

只覆盖等于(除了@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() !!

那么你就错过了基于哈希的集合的要点。 考虑以下几点:

Your hashCode() implementation : intObject%9.

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

Bucket 1 : 1,10,19,... (in thousands)
Bucket 2 : 2,20,29...
Bucket 3 : 3,21,30,...
...

假设,您想知道映射是否包含键10。 你想把所有的桶都搜一遍吗?或“是否只搜索一个桶?”

根据hashCode,可以确定如果存在10,则它必须存在于Bucket 1中。 所以只有桶1会被搜索!!


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

首先,从更广泛的角度来看,我们有集合,而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());
    }
}

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

对于网球-黄色,红色。 板球-白色

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

给球上色-哈希。 选择比赛的球-平等。

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


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()来查找Entry中的值

如果两者都被覆盖,

地图<A>

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

If =没有被覆盖

地图<A>

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

如果hashCode没有被覆盖

地图<A>

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等价契约

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


身份不是平等。

= operator ==测试标识符。 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生成整数,以便将对象存储在HashMap、HashSet等数据结构中。

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

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

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

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


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

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

Isn't this the wrong way round? Overriding hashCode likely implies you're writing a hash-key class, but overriding equals certainly does not. There are many classes that are not used as hash-keys, but do want a logical-equality-testing method for some other reason. If you choose "equals" for it, you may then be mandated to write a hashCode implementation by overzealous application of this rule. All that achieves is adding untested code in the codebase, an evil waiting to trip someone up in the future. Also writing code you don't need is anti-agile. It's just wrong (and an ide generated one will probably be incompatible with your hand-crafted equals).

他们肯定应该在被写来用作键的对象上强制设置一个接口吗?无论如何,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()

public boolean equals(对象obj)

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

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

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

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

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

对于任何非空参考值x, x.equals(null)应该返回 假的。

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。


常见错误如下例所示。

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()之间的契约是:

如果两个对象相等,那么它们必须具有相同的哈希码。 如果两个对象具有相同的哈希码,则它们可能相等,也可能不相等。 公共int hashCode(){ 返回this.color.hashCode (); }


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

It leads problem when you use Map collection and the key is of Persistent type, StringBuffer/builder type. Since they don't override equals() and hashCode() unlike String class, equals() will return false when you compare two different objects even though both have same contents. It will make the hashMap storing same content keys. Storing same content keys means it is violating the rule of Map because Map doesnt allow duplicate keys at all. Therefore you override equals() as well as hashCode() methods in your class and provide the implementation(IDE can generate these methods) so that they work same as String's equals() and hashCode() and prevent same content keys.

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

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

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

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

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

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

就是这样!


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

Since hashcode always returns a number its always fast to retrieve an object using a number rather than an alphabetic key. How will it do? Assume we created a new object by passing some value which is already available in some other object. Now the new object will return the same hash value as of another object because the value passed is same. Once the same hash value is returned, JVM will go to the same memory address every time and if in case there are more than one objects present for the same hash value it will use equals() method to identify the correct object.


当您希望将自定义对象作为Map中的键存储和检索时,您应该始终重写自定义对象中的equals和hashCode。 例如:

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只被认为是一个对象,映射大小也只有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库提供了EqualsBuilder和HashCodeBuilder类。这些类极大地简化了复杂类的equals()和hashCode()方法的实现。

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


为什么重写equals()方法

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

operator ==如何工作。

它检查我们比较的两个引用是否指向内存中的同一个实例。只有当这两个引用表示内存中的同一个实例时,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关键字创建的,所以这些实例位于内存中的不同段中。因此运算符==将返回false

但是如果我们不能重写==操作符,我们怎么能对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()的自定义版本(也就是重写),那么Object类和==操作符中预定义的.equals()将表现完全相同。

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

为什么重写hashCode()方法

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

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

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

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

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

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

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

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

当我们在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()很有可能得到不同的结果。

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

这违背了HashMap的逻辑

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

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

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

现在我们来解决这个问题。让我们重写hashCode()方法来考虑equals()考虑的相同字段,即年龄、姓名

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

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