Автор: Widowmaker1984

Вопросы 69-78 (класс Object и его методы equals, hashCode, toString)

"Как связан любой пользовательский класс с классом Object?"
"Расскажите про каждый из методов класса Object."
"Что такое метод equals(). Чем он отличается от операции ==."
"Если вы хотите переопределить equals(), какие условия должны удовлетворяться для переопределенного метода?"
"Если equals() переопределен, есть ли какие-либо другие методы, которые следует переопределить?"
"В чем особенность работы методов hashCode и equals? Каким образом реализованы методы hashCode и equals в классе Object? Какие правила и соглашения существуют для реализации этих методов? Когда они применяются?"
"Какой метод возвращает строковое представление объекта?"
"Что будет, если переопределить equals не переопределяя hashCode? Какие могут возникнуть проблемы?"
"Есть ли какие-либо рекомендации о том, какие поля следует использовать при подсчете hashCode?"
"Как вы думаете, будут ли какие-то проблемы, если у объекта, который используется в качестве ключа в hashMap изменится поле, которое участвует в определении hashCode?"


1. В Java есть специальный суперкласс Object и все классы являются его подклассами (это не требуется указывать явно). Поэтому ссылочная переменная класса Object может ссылаться на объект любого другого класса.

1.1. Так как массивы являются тоже классами, то переменная класса Object может ссылаться и на любой массив.

2. У класса есть несколько важных методов:

скрытый текстprotected native Object clone() throws CloneNotSupportedException — создаёт новый объект, не отличающий от клонируемого;
public boolean equals(Object obj) — определяет, равен ли один объект другому;
protected void finalize() throws Throwable — вызывается сборщиком мусора, когда он определил, что ссылок на объект больше нет;
public final native Class getClass() — получает класс объекта во время выполнения;
public native int hashCode() — возвращает хеш-код, связанный с вызывающим объектом;
public final native void notify() — возобновляет выполнение одного потока, который ожидает вызывающего объекта;
public final native void notifyAll() — возобновляет выполнение всех потоков, которые ожидают вызывающего объекта;
public String toString() — возвращает строковое представление объекта;
public final void wait() throws InterruptedException — приводит данный поток в ожидание, пока другой поток не вызовет notify() или notifyAll() методы для этого объекта.;
public final native void wait(long timeout) throws InterruptedException --приводит данный поток в ожидание, пока другой поток не вызовет notify() или notifyAll() для этого метода, или пока не истечет указанный промежуток времени.
public final void wait(long timeout, int nanos) throws InterruptedException — приводит данный поток в ожидание, пока другой поток не вызовет notify() или notifyAll() для этого метода, или пока не истечет указанный промежуток времени.

3. Хеш-код - это целочисленный результат работы метода, которому в качестве входного параметра передан объект. Этот метод реализован таким образом, что для одного и того-же входного объекта, хеш-код всегда будет одинаковым. Следует понимать, что множество возможных хеш-кодов ограничено примитивным типом int, а множество объектов ограничено только нашей фантазией. Отсюда следует утверждение: “Множество объектов мощнее множества хеш-кодов”. Из-за этого ограничения, вполне возможна ситуация, что хеш-коды разных объектов могут совпасть.

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

3.2. У любого объекта имется хеш-код, определяемый по умолчанию, который вычисляется по адресу памяти, занимаемой объектом.

3.2.1. А, например, для вычисления хеш-кода в классе String применяется следующий алгоритм:

int hash = 0;
for(int i = 0; i < length(); i++)
hash = 31 * hash + charAt(i);

3.3. Выводы:
- Для одного и того-же объекта, хеш-код всегда будет одинаковым.
- Если объекты одинаковые (т.е. одного класса с одинаковым содержимым полей.), то и хеш-коды одинаковые.
- Если хеш-коды равны, то входные объекты не всегда равны (коллизия).
- Если хеш-коды разные, то и объекты гарантированно разные.

4. В Java, каждый вызов оператора new порождает в памяти новый объект, при этом их содержимое может быть одинаково, то есть эквивалентно. Для проверки эквивалентности в классе Object существует метод equals(), который сравнивает содержимое объектов и выводит значение true, если содержимое эквивалентно, и false — если нет.

object1.equals(object2);

4.1. Эквивалентность и хеш-код тесно связанны между собой, поскольку хеш-код вычисляется на основании содержимого объекта (значения полей) и если у двух объектов одного и того же класса содержимое одинаковое, то и хеш-коды по идее должны быть одинаковыми.

4.2. Но на самом деле по умолчанию код метода equals следующий: return (this == obj);
При сравнение объектов, операция “==” вернет true лишь в одном случае — когда ссылки указывают на один объект. В данном случае не учитывается содержимое полей.

4.2.1. При вычислении хэш-кода для объектов класса Object по умолчанию используется Park-Miller RNG алгоритм. В основу работы данного алгоритма положен генератор случайных чисел. Это означает, что при каждом запуске программы у объекта будет разный хэш-код. Получается, что используя реализацию метода hashCode() от класса Object, мы при каждом создании объекта будем получать разные хеш-коды. Мало того, перезапуская программу, мы будем получать абсолютно разные значения, поскольку это просто случайное число.

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

5.1. Например, в классе String метод equals переопределяется таким образом, что возвращается true, если содержимое двух сравниваемых строк одинаковое. А в классе-обертке Integer метод equal переопределяется для выполнения численного сравнения.

5.2. Если переопределить equals не переопределяя hashCode объекты данного класса могут неправильно хранится в контейнерах, использующих данные методы при вставке и извлечении объектов, таких как HashMap, HashTable или HashSet.

5.2.1. Для правильной работы этих компонентов Java предлагает следующее правило для переопределения указанных методов: эквивалентным называется отношение, которое является симметричным, транзитивным и рефлексивным.

Рефлексивность: для любого ненулевого x, x.equals(x) вернет true;
Транзитивность: для любого ненулевого x, y и z, если x.equals(y) и y.equals(z) вернет true, тогда и x.equals(z) вернет true;
Симметричность: для любого ненулевого x и y, x.equals(y) должно вернуть true, тогда и только тогда, когда y.equals(x) вернет true.

5.2.1.1. Повторный вызов метода equals() должен возвращать одно и тоже значение до тех пор, пока какое-либо значение свойств объекта не будет изменено. То есть, если два объекта равны в Java, то они будут равны пока их свойства остаются неизменными.

5.2.1.2. Также для любого ненулевого x, x.equals(null) должно вернуть false.

5.2.2. Соглашение между equals и hashCode в Java:
1) Если объекты равны по результатам выполнения метода equals, тогда их hashcode должны быть одинаковыми.
2) Если объекты не равны по результатам выполнения метода equals, тогда их hashcode могут быть как одинаковыми, так и разными. Однако для повышения производительности, лучше, чтобы разные объекты возвращали разные коды.

5.3. Рекомендации как переопределять метод equals в Java (сравнение с объектом obj):
- Проверьте, не указывают ли ссылки на один объект.
- Проверьте объект на null, а также проверьте, чтобы объекты были одного типа. Не делайте проверку с помощью instanceof так как такая проверка будет возвращать true для подклассов. Вместо этого можно использовать getClass();
- Объявите переменную типа, который вы сравниваете, и приведите obj к этому типу. Потом сравнивайте каждый атрибут типа начиная с численных атрибутов (если имеются) потому что численные атрибуты проверяются быстрей. Сравнивайте атрибуты с помощью операторов И и ИЛИ для объединения проверок с другими атрибутами.

5.4. При подсчете хеш-кода необходимо использовать уникальные, лучше примитивные поля, такие как id или uuid. Причем, если эти поля задействованы при вычислении hashCode, их задействовать и при выполнении equals. Более общий совет: выбирать поля, которые с большой долью вероятности будут различаться. Хеш-код должен быть равномерно распределен на области возможных принимаемых значений.

5.4.1. Если у объекта, который используется в качестве ключа в hashMap изменится поле, которое участвует в определении хеш-кода, то могут возникнуть проблемы с поиском значения по ключу.

6. Метод toString возвращает строковое представление объекта.

6.1. Очень часто при использовании метода toString() для получения описания объекта можно получить набор бессмысленных символов, например, [I@421199e8. На самом деле в них есть смысл, доступный специалистом. Он сразу может сказать, что мы имеем дело с одномерным массивом (одна квадратная скобка), который имеет тип int (символ I). Остальные символы тоже что-то означают, но вам знать это не обязательно.

6.2. По умолчанию метод работает по следующему алгоритму:
getClass().getName() + '@' + Integer.toHexString(hashCode())

6.2.1. Принято переопределять метод, чтобы он выводил результат в читаемом виде.

Комментарии


Лучшее   Правила сайта   Вход   Регистрация   Восстановление пароля

Материалы сайта предназначены для лиц старше 16 лет (16+)