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

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

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


当前回答

加上@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会被搜索!!

其他回答

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.

在下面的例子中,如果您注释掉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);
        }
    }
}

为什么重写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键。

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

常见错误如下例所示。

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 (); }

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)将为假。

希望这是清楚的