Почему мне нужно переопределить методы equals и hashCode в Java? - PullRequest
336 голосов
/ 15 февраля 2010

Недавно я прочитал это Документ разработчика работ .

Документ посвящен эффективному и правильному определению hashCode() и equals(), однако я не могу понять, почему мы должны переопределить эти два метода.

Как я могу принять решение об эффективной реализации этих методов?

Ответы [ 28 ]

485 голосов
/ 15 февраля 2010

Джошуа Блох говорит об эффективной Java

Вы должны переопределить hashCode () в каждом классе, который переопределяет equals (). Невыполнение этого требования приведет к нарушению общего контракта для Object.hashCode (), что помешает правильной работе вашего класса в сочетании со всеми коллекциями на основе хешей, включая HashMap, HashSet и Hashtable.

Давайте попробуем понять это на примере того, что произойдет, если мы переопределим equals() без переопределения hashCode() и попытаемся использовать Map.

Скажем, у нас есть такой класс, и что два объекта MyClass равны, если их importantField равны (с 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;
    }

    public String getEqualField() {
        return importantField;
    }

    public String getAnotherField() {
        return 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;
    }

}

Только переопределение equals

Если только equals переопределено, то при вызове myMap.put(first,someValue) сначала будет хешироваться в какой-то сегмент, а при вызове myMap.put(second,someOtherValue) он будет хешироваться в другой сегмент (так как у них другой hashCode). Таким образом, хотя они равны, поскольку они не хешируют одно и то же ведро, карта не может этого понять, и они оба остаются на карте.


Хотя нет необходимости переопределять equals(), если мы переопределим hashCode(), давайте посмотрим, что произойдет в этом конкретном случае, когда мы знаем, что два объекта MyClass равны, если их importantField равны, но мы не переопределять equals().

Только переопределение hashCode

Представьте, что у вас есть это

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

Если вы переопределяете только hashCode, то при вызове myMap.put(first,someValue) он занимает первое место, вычисляет его hashCode и сохраняет его в заданном сегменте. Затем, когда вы звоните myMap.put(second,someOtherValue), он должен заменить первое на второе согласно Документация карты , потому что они равны (согласно бизнес-требованиям).

Но проблема в том, что функция equals не была переопределена, поэтому, когда карта хэширует second и выполняет итерацию по корзине, глядя, есть ли объект k такой, что second.equals(k) является истинным, он не найдет его как second.equals(first) будет false.

Надеюсь, это было ясно

190 голосов
/ 27 ноября 2014

Коллекции, такие как HashMap и HashSet, используют значение хеш-кода объекта, чтобы определить, как он должен храниться в коллекции, а хэш-код снова используется в Для того, чтобы найти объект в своей коллекции.

Хеширование - это двухэтапный процесс:

  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

Теперь вы можете видеть, почему, если два объекта считаются равными, их хеш-код s должен также быть равным? В противном случае вы никогда не сможете найти объект, так как по умолчанию hashcode метод в классе Object практически всегда имеет уникальный номер для каждого объекта, даже если метод equals() переопределен таким образом, что два или более объектов считаются равными. Неважно, насколько равны объекты, если их хеш-код s не отражают это. Итак, еще раз: если два объекта равны, их хеш-код s также должен быть равен.

48 голосов
/ 15 февраля 2010

Вы должны переопределить hashCode () в каждом класс, который переопределяет равно (). недостаточность это приведет к нарушению генеральный контракт на Object.hashCode (), который будет препятствовать ваш класс не работает должным образом в сочетании со всеми основанными на хэше коллекции, в том числе HashMap, HashSet и Hashtable.


от Эффективная Java , Джошуа Блох

Путем последовательного определения equals() и hashCode() вы можете улучшить удобство использования ваших классов в качестве ключей в коллекциях на основе хеша. Как объясняет документ API для hashCode: «Этот метод поддерживается для использования хеш-таблиц, таких как те, которые предоставляются java.util.Hashtable».

Лучший ответ на ваш вопрос о том, как эффективно реализовать эти методы, предлагает вам прочитать главу 3 Effective Java .

20 голосов
/ 15 февраля 2010

Проще говоря, метод equals в 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) имеет значение true, поскольку это тот же экземпляр
  • b.equals (b) имеет значение true, поскольку это тот же экземпляр

Правильно? Ну, может быть, если это то, что вы хотите. Но допустим, что мы хотим, чтобы объекты с одинаковым идентификатором были одним и тем же объектом, независимо от того, являются ли они двумя разными экземплярами. Мы переопределяем равенства (и хэш-код):

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

15 голосов
/ 16 июля 2015

Идентичность не равенство.

  • равно оператору == проверка личности.
  • equals(Object obj) метод сравнивает тест на равенство (т. Е. Нам нужно определить равенство, переопределив метод)

Почему мне нужно переопределить методы equals и hashCode в Java?

Сначала мы должны понять использование метода равных.

Чтобы идентифицировать различия между двумя объектами, нам нужно переопределить метод 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 .

Предположим, у нас есть метод переопределения равно Customer, как указано выше,

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

При работе со структурой данных, когда мы храним объект в контейнерах (bucket - причудливое имя для папки). Если мы используем встроенную технику хеширования, для более чем двух клиентов она генерирует два разных хеш-кода. Таким образом, мы храним один и тот же объект в двух разных местах. Чтобы избежать подобных проблем, мы должны переопределить метод hashCode, также основанный на следующих принципах.

  • неравные экземпляры могут иметь одинаковый хеш-код.
  • равные экземпляры должны возвращать тот же хеш-код.
12 голосов
/ 03 марта 2015

Хорошо, позвольте мне объяснить концепцию очень простыми словами.

Во-первых, в более широком плане у нас есть коллекции, и hashmap является одной из структур данных в коллекциях.

Чтобы понять, почему мы должны переопределить оба метода equals и hashcode, если нужно сначала понять, что такое hashmap и что делает.

Хеш-карта - это структура данных, которая хранит пары ключевых значений данных в виде массива. Скажем, [], где каждый элемент в 'a' является парой ключ-значение.

Также каждый индекс в вышеуказанном массиве может быть связанным списком, таким образом, имея более одного значения в одном индексе.

Теперь, почему используется hashmap? Если нам нужно искать среди большого массива, тогда поиск по каждому из них, если они не будут эффективными, так что метод хэширования говорит нам, что позволяет предварительно обработать массив с некоторой логикой и сгруппировать элементы на основе этой логики, то есть хеширование

Например: у нас есть массив 1,2,3,4,5,6,7,8,9,10,11, и мы применяем хеш-функцию mod 10, поэтому 1,11 будут сгруппированы вместе. Поэтому, если бы нам пришлось искать 11 в предыдущем массиве, нам пришлось бы выполнять итерацию всего массива, но когда мы группируем его, мы ограничиваем область итерации, тем самым повышая скорость. Эту структуру данных, используемую для хранения всей вышеупомянутой информации, для простоты можно представить как двумерный массив

Теперь кроме вышеприведенного хэш-карты также сказано, что он не будет добавлять в него Дубликаты. И это главная причина, почему мы должны переопределить equals и hashcode

Итак, когда сказано, что объясняют внутреннюю работу hashmap, нам нужно найти, какие методы есть у hashmap и как она следует вышеприведенным правилам, которые я объяснил выше

таким образом, в hashmap есть метод, называемый как put (K, V), и в соответствии с hashmap он должен следовать приведенным выше правилам эффективного распределения массива и не добавлять дубликаты

что делает, так это то, что он сначала сгенерирует хеш-код для данного ключа, чтобы решить, в какой индекс должно входить значение. Если в этом индексе ничего нет, тогда новое значение будет добавлено туда, если что-то уже присутствует там, то новое значение должно быть добавлено после конца связанного списка в этом индексе. но помните, что дубликаты не должны добавляться в соответствии с желаемым поведением хэш-карты. Допустим, у вас есть два объекта Integer aa = 11, bb = 11. Так как каждый объект является производным от класса объекта, реализация по умолчанию для сравнения двух объектов состоит в том, что он сравнивает ссылку, а не значения внутри объекта. Таким образом, в вышеприведенном случае оба, хотя и семантически равные, не пройдут проверку на равенство, и вероятность того, что два объекта с одинаковым хеш-кодом и одинаковыми значениями будут существовать, создаст дубликаты. Если мы переопределим, мы могли бы избежать добавления дубликатов. Вы также можете сослаться на Детальная работа

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


}

}
11 голосов
/ 29 июля 2013

hashCode():

Если вы переопределите только метод хеш-кода, ничего не произойдет. Потому что он всегда возвращает новый hashCode для каждого объекта в виде класса Object.

equals():

Если вы переопределяете только равный метод, a.equals(b) имеет значение true, это означает, что hashCode для a и b должны быть одинаковыми, но не происходить Потому что вы не переопределили метод hashCode.

Примечание: hashCode() метод класса Object всегда возвращает новый hashCode для каждого объекта.

Поэтому, когда вам нужно использовать свой объект в коллекции на основе хеширования, необходимо переопределить как equals(), так и hashCode().

6 голосов
/ 09 декабря 2014

Добавление к ответу @Lombo

Когда вам нужно переопределить равно ()?

Реализация по умолчанию Object's equals ():

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

, что означает, что два объекта будут считаться равными, только если они имеют один и тот же адрес памяти, что будет истинно, только если вы сравнивая объект с самим собой.

Но вы можете рассматривать два объекта одинаково, если они имеют одинаковое значение для одного или больше их свойств (см. пример, приведенный в ответе @Lombo).

Таким образом, вы переопределите equals() в этих ситуациях и предоставите свои собственные условия равенства.

Я успешно реализовал функцию equals (), и она отлично работает. Так почему же они просят переопределить hashCode ()?

Хорошо. Пока вы не используете Коллекции, основанные на "хэше" в вашем пользовательском классе, это нормально. Но когда-нибудь в будущем вы, возможно, захотите использовать HashMap или HashSet, и если вы не override и "правильно реализуете" hashCode () , эта коллекция на основе хэша не будет работать как и предполагалось.

Переопределить только равно (дополнение к ответу @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 с first. Только если значения совпадают, он будет проверять равенство в том же сегменте.

Но здесь hashCode отличается для этих двух объектов (потому что они имеют разные адреса памяти - от реализации по умолчанию). Следовательно, он даже не захочет проверять равенство.

Если у вас есть точка останова внутри вашего переопределенного метода equals (), он не вступит, если у них разные хэш-коды. 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. Хотите обыскать все ведра? или вы хотите искать только одно ведро?

На основании хэш-кода вы должны определить, что если 10 присутствует, он должен присутствовать в сегменте 1. Поэтому будет выполняться поиск только в Bucket 1 !!

6 голосов
/ 15 февраля 2010

Потому что, если вы не переопределите их, вы будете использовать имплантацию по умолчанию в Object.

Учитывая, что равенство экземпляров и значения hascode обычно требуют знания того, из чего состоит объект, они, как правило, должны быть переопределены в вашем классе, чтобы иметь какое-либо ощутимое значение.

6 голосов
/ 11 марта 2014

Чтобы использовать наши собственные объекты классов в качестве ключей в коллекциях, таких как HashMap, Hashtable и т. Д., Мы должны переопределить оба метода (hashCode () и equals ()), имея представление о внутренней работе коллекции. В противном случае это приведет к неверным результатам, которых мы не ожидаем.

...