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

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

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


当前回答

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.

其他回答

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

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

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

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

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

身份不是平等。

= 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。

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

图片