- Методы .equals и .hashcode в Java. Отличия реализации по умолчанию от реализации на практике
- Метод .equals()
- Отношение эквивалентности (алгебра)
- Реализация .equals() по умолчанию
- Как и зачем переопределяют метод .equals()?
- Метод .hashcode()
- Сюръекция (алгебра)
- Что происходит в java?
- Реализация .hashcode() по умолчанию?
- Как и зачем переопределяют метод .hashcode()?
- Перегрузка методов equals() и hashCode() в Java
- Переопределение методов equals() и hashCode() в Java
- Соглашение между equals и hashCode в Java
- Как переопределять метод equals в Java
- Распространенные ошибки при переопределении equals в Java
- Подсказки как писать в Java метод equals
Методы .equals и .hashcode в Java. Отличия реализации по умолчанию от реализации на практике
Сразу же скажу, что статья во многом опирается базовые понятия алгебры, которые к великому счастью легко и быстро осознаются при помощи всего-лишь метода внимательного разглядывания. Поехали.
В Java так устроено, что любой класс, который вы определяете, наследуется от класса Object. Таким образом класс Object является суперклассом любого класса в любой программе.
Это означает, что абсолютно любой класс содержит методы, которые определены в классе Object. Методы .equals() и .hashcode() — одни из них.
Прежде всего я должен описать главные правила для любых реализаций этих двух методов, которые нужно обязательно соблюдать, запомнить как аксиому:
1). Если x.equals(y) == true, то обязательно hashcode(x) == hashcode(y)
2) Если hashcode(x) == hashcode(y), то не обязательно x.equals(y) == true
Метод .equals()
Отношение эквивалентности (алгебра)
Прежде чем поговорить о методе .equals, я бы хотел рассказать, что такое отношение эквивалентности с точки зрения алгебры (пока-что забудьте про программирование).
Отношение эквивалентности — это бинарное (бинарное — значит между двумя) отношение, которое является:
Таким образом, если на множестве определено отношение эквивалентности, множество можно разделить на подмножества — классы эквивалентности.
Каждый класс эквивалентности содержит внутри себя только те элементы, которые эквиваленты (более формально — находятся в отношении эквивалентности) между собой.
Реализация .equals() по умолчанию
Метод .equals() в классе Object реализован примерно следующим образом:
public boolean equals(Object x)
Фактически он делает следующее: Он принимает в качестве аргумента ссылочную переменную и проверяет, ссылается ли они на тот же объект (ту же область памяти, если быть точнее), что и объект, к которому мы применили метод .equals().
Таким образом, стандартная реализация .equals() выстраивает отношение эквивалентности, которое можно описать так: две ссылки эквивалентны, если они ссылаются на одну и ту же область памяти.
Такая реализация не противоречит математической идеологии, описанной выше. Однако на практике метод .equals() часто переопределяют в подклассах.
Как и зачем переопределяют метод .equals()?
Очевидно, гораздо более применимой будет возможность сравнивать объекты по какому-нибудь другому критерию. Часто метод .equals() переопределяют так, чтобы он сравнивал объекты по значениям их полей.
К примеру, если классы двух объектов, на которые указывают ссылки, совпадают и все значения их полей совпадают, то эти два объекта эквивалентны между собой. Легко проследить, что такое определение не противоречит математической идеологии.
Конкретную кодовую реализацию я приводить не буду, потому что она не так важна, как сама идея
Это и другие возможные переопределения метода .equals() мало того, что расширяют круг наших возможностей, так ещё и не лишают старых, ведь мы по прежнему имеем возможность проверять, ссылаются ли две ссылки на одну область памяти, используя операнд ==, вместо прежнего .equals()
Метод .hashcode()
Сюръекция (алгебра)
Сюръекция — сопоставление элементам множества X элементов второго множества Y, при котором для любого элемента из Y есть хотя-бы один сопоставленный элемент из X.
Если немного более подробно разобрать это определение, то мы увидим следующее:
- Даже несколько элементов из X могут быть сопоставлены одному и тому же элементу из Y (это называется коллизией).
- Возможно есть такое элемент из X, и даже возможно не один, что он не сопоставлен никакому элементу из Y. (см. рисунок, всё интуитивно)
Что происходит в java?
Метод .hashcode() как-раз осуществляет сюръекцию. Множеством X выступает множество всевозможных объектов которые мы можем создать, множеством Y выступает область значений типа данных int. Метод .hashcode() вычисляет каким-то скрытым от нас способом целое число, опираясь на объект, к которому применяется.
Единственное отличие метода .hashcode() от сюръекции в том, что любой объект может быть обработан методом .hashcode()
Реализация .hashcode() по умолчанию?
Насколько я понял, точно так никто в этом и не разобрался. Есть много версий:
- Значение .hashcode() — это область памяти, где лежит объект
- Значение .hashcode() — это число, создаваемое генератором случайных чисел в какой-то момент
- Сама функция написана не на Java а вообще на C.
И многие другие. В общем каким-то образом она всё же устроена, но самое главное в том, что стандартная реализация .hashcode() со стандартной реализацией .equals() подчиняются правилу, приведённому в самом начале статьи
Как и зачем переопределяют метод .hashcode()?
Основной причиной для изменения метода .hashcode() является то, что желают изменить .equals(), однако смена стандартной реализации .equals() приводит к нарушению правила из начала статьи
Второстепенной причиной для изменения метода .hashcode() является то, что желают изменить вероятность коллизии (эта причина встречается реже)
Перегрузка методов equals() и hashCode() в Java
Переопределение методов equals() и hashCode() в Java
Equals и hashCode являются фундаментальными методами объявленные в классе Object и содержатся в стандартных библиотеках Java. Метод еquals() используется для сравнения объектов, а hashCode — для генерации целочисленного кода объекта. Эти методы широко используются в стандартных библиотеках Java при вставке и извлечении объектов в HashMap . Метод equal также используется для обеспечения хранения только уникальных объектов в HashSet и других Set реализациях, а также в любых других случаях, когда нужно сравнивать объекты. Реализация по умолчанию метода equals() в классе java.lang.Object сравнивает ссылки на адреса в памяти, которые хранят переменные, и возвращает true только в том случае, если адреса совпадают, другими словами переменные ссылаются на один и тот же объект. Java рекомендует переопределять методы equals() и hashCode() , если предполагается, что сравнение должно осуществляться в соответсвии с естественной логикой или бизнес-логикой. Многие классы в стандартных библиотеках Java переопределяют их, например в классе String переопределяется equals таким образом, что возвращается true , если содержимое двух сравниваемых объектов одинаковое. В классе-обертке Integer метод equal переопределяется для выполнения численного сравнения, и так далее. Так как HashMap и HashTable в Java полагаются на методы equals() и hashCode() для сравнения своих key и values , то Java предлагает следующие правила для переопределения этих методов:
- Рефлексивность: Объект должен равняться себе самому.
- Симметричность: если a.equals(b) возвращает true , то b.equals(a) должен тоже вернуть true .
- Транзитивность: если a.equals(b) возвращает true и b.equals(c) тоже возвращает true , то c.equals(a) тоже должен возвращать true .
- Согласованность: повторный вызов метода equals() должен возвращать одно и тоже значение до тех пор, пока какое-либо значение свойств объекта не будет изменено. То есть, если два объекта равны в Java, то они будут равны пока их свойства остаются неизменными.
- Сравнение null : объект должны быть проверен на null . Если объект равен null , то метод должен вернуть false , а не NullPointerException . Например, a.equals(null) должен вернуть false .
Соглашение между equals и hashCode в Java
- Если объекты равны по результатам выполнения метода equals , тогда их hashcode должны быть одинаковыми.
- Если объекты не равны по результатам выполнения метода equals , тогда их hashcode могут быть как одинаковыми, так и разными. Однако для повышения производительности, лучше, чтобы разные объекты возвращали разные коды.
Как переопределять метод equals в Java
@Override public boolean equals(Object obj) < /*1. Проверьте*/if (obj == this) < /*и верните */ return true; >
if (obj == null || obj.getClass() != this.getClass())
Person guest = (Person) obj; return guest.id && (firstName == guest.firstName || (firstName != null && firstName.equals(guest.getFirstName()))) && (lastName == guest.lastName || (lastName != null && lastName .equals(guest.getLastName()))); >
/** * Person class with equals and hashcode implementation in Java * @author Javin Paul */ public class Person < private int id; private String firstName; private String lastName; public int getId() < return id; >public void setId(int id) < this.id = id;>public String getFirstName() < return firstName; >public void setFirstName(String firstName) < this.firstName = firstName; >public String getLastName() < return lastName; >public void setLastName(String lastName) < this.lastName = lastName; >@Override public boolean equals(Object obj) < if (obj == this) < return true; >if (obj == null || obj.getClass() != this.getClass()) < return false; >Person guest = (Person) obj; return guest.id && (firstName == guest.firstName || (firstName != null &&firstName.equals(guest.getFirstName()))) && (lastName == guest.lastName || (lastName != null && lastName .equals(guest.getLastName()) )); > @Override public int hashCode() < final int prime = 31; int result = 1; result = prime * result + ((firstName == null) ? 0 : firstName.hashCode()); result = prime * result + id; result = prime * result + ((lastName == null) ? 0 : lastName.hashCode()); return result; >>
Распространенные ошибки при переопределении equals в Java
- Вместо того, чтобы переопределять метод equals (Override) программист перегружает его (Overload) Синтаксис метода equals() в классе Object определен как public boolean equals(Object obj) , но многие программисты ненароком перегружают метод: public boolean equals(Person obj) — вместо Object в качестве аргумента используют имя своего класса (напр. Person). Эту ошибку сложно обнаружить из-за static binding . Таким образом, если вы вызовете этот метод для объекта своего класса, то метод не просто скомпилируется, а даже сделает это корректно. Однако, если вы положите ваш объект в коллекцию, например ArrayList и вызовете метод contains() , работа которого основана на методе equals() , то метод contains не сможет обнаружить ваш объект.
- При переопределении метода equals() не проверять на null переменные, что в конечном итоге заканчивается NullPointerException при вызове equals() . Ниже представлен корректный код.
firstname == guest.firstname || (firstname != null && firstname.equals(guest.firstname));
Подсказки как писать в Java метод equals
- Большинство IDE такие как NetBeans, Eclipse и IntelliJ IDEA обеспечивают поддержку генерации методов equals() и hashCode() . В Eclipse нажмите правую кнопку -> source -> generate equals() и hashCode() .
- Если в классе есть уникальный бизнес-ключ, то будет достаточно сделать проверку только на равенство этих полей. Как в нашем примере “id” — уникальный номер для каждого Person.
- При переопределении hashCode() в Java удостоверьтесь в использовании всех полей, что были использованы в методе equals() .
- String и классы-оболочки такие как Integer , Float и Double переопределяют метод equals() , но StringBuffer не переопределяет.
- При любой возможности делайте поля immutable используя final переменные в Java.
- При сравнении String объектов используйте equals() вместо оператора == .
- Два объекта которые логически равны, но загружены из разных ClassLoader не могут быть равными. Помните, что проверка с помощью getClass() вернет false если класс-загрузчик разный.
- Используйте @Override аннотацию также для метода hashCode , так как это предупреждает неуловимые ошибки, например возвращаемое значение метода int , однако некоторые программисты возвращают long .