1. Сравнение объектов в Java
Объекты в Java можно сравнивать как по ссылке, так и по значению.
Сравнение ссылок
Если две переменные указывают на один и тот же объект в памяти, то ссылки, которые хранятся в этих переменных равны. Если сравнить такие переменные с помощью оператора равенства ==
, вы получите true, что логично. Тут все просто.
Код | Вывод на экран |
---|---|
|
|
Сравнение по значению
Однако часто можно встретить ситуацию, когда две переменные ссылаются на два разных, но идентичных объекта. Например, две строки, которые содержат одинаковый текст, но находятся в различных объектах.
Для определения идентичности разных объектов нужно использовать метод equals()
Пример:
Код | Вывод на экран |
---|---|
|
|
Метод equals
есть не только у класса String
— он есть у вообще всех классов.
Даже у тех, которые вы только будете писать, и вот почему.
2. Класс Object
Все классы в Java считаются унаследованными от класса Object
. Это создатели Java так придумали.
А если некий класс унаследован от класса Object
, в этом классе-наследнике появляются все методы класса Object
. Это и есть главный эффект наследования.
Другими словами, у каждого класса, даже если это не написано в его коде, есть все методы, которые есть у класса Object
.
А среди таких методов есть методы, которые имеют отношение к сравнению объектов. Это метод equals()
и метод hashCode()
.
Код | Как будет на самом деле: |
---|---|
|
|
В примере выше мы создали простой класс Person
с параметрами name и age, без единого метода. Однако, т.к. все классы считаются унаследованными от класса Object
, у класса Person
скрытно появились два метода:
Метод | Описание |
---|---|
|
Сравнивает текущий объект и переданный объект |
|
Возвращает hash-code текущего объекта |
Получается, что методы equals
есть у абсолютно всех объектов и можно сравнивать между собой объекты разных типов, и это все будет отлично компилироваться и работать.
Код | Вывод на экран |
---|---|
|
|
|
|
3. Метод equals()
Унаследованный от класса Object
метод equals()
содержит самый простой алгоритм сравнивания текущего и переданного объектов — он просто сравнивает их ссылки.
Тот же эффект вы получите, если просто сравните переменные класса Person
вместо вызова метода equals().
Пример:
Код | Вывод на экран |
---|---|
|
|
Метод equals
просто сравнивает у себя внутри ссылки a
и b
.
Однако у класса String
сравнение работает по-другому. Почему?
Потому что разработчики класса String
написали собственную реализацию метода equals()
.
Реализация метода equals()
Давайте и мы напишем свою реализацию метода equals в классе Person
. Разберем 4 основных случая.
equals
, он всегда принимает параметр типа Object
Сценарий 1: в метод equals
передали тот же самый объект, у которого вызвали метод equals
. Если ссылки у текущего и переданного объектов равны, нужно вернуть true
. Объект совпадает сам с собой.
В коде это будет выглядеть так:
Код | Описание |
---|---|
|
Сравниваем ссылки |
Сценарий 2: в метод equals
передали ссылку null
— сравнивать не с чем. Объект, у которого вызвали метод equals
, точно не null, значит, в этом случае нужно вернуть false
.
В коде это будет выглядеть так:
Код | Описание |
---|---|
|
Сравниваем ссылки Переданный объект — null ?
|
Сценарий 3: в метод equals
передали ссылку на объект вообще не класса Person
. Равен ли объект класса Person
объекту класса не-Person
? Тут уже решает сам разработчик класса Person
— как хочет, так и сделает.
Но обычно все же объекты считаются равными, если это объекты одного класса. Поэтому если в наш метод equals передали объект не класса Person
, мы будем всегда возвращать false
. А как проверить, какого типа объект? Правильно: с помощью оператора instanceof
.
Вот как будет выглядеть наш новый код:
Код | Описание |
---|---|
|
Сравниваем ссылки Переданный объект — null ?
Если переданный объект не типа Person |
4. Сравнение двух объектов Person
Что мы получили в итоге? Если мы дошли до конца метода, значит, у нас объект типа Person
и ссылка не null
. Тогда преобразовываем его к типу Person
и будем сравнивать внутренности обоих объектов. Это и есть наш сценарий номер 4.
Код | Описание |
---|---|
|
Сравниваем ссылки Переданный объект — null ?
Если переданный объект не типа Person
Операция приведения типа |
А как сравнивать два объекта Person
? Они равны, если у них равны имена (name
) и возраст (age
). Итоговый код будет выглядеть так:
Код | Описание |
---|---|
|
Сравниваем ссылки Переданный объект — null ?
Если переданный объект не типа Person
Операция приведения типа |
Но и это еще не все.
Во-первых, поле name имеет тип String
, а значит, поля name нужно сравнивать с помощью вызова метода equals
.
this.name.equals(person.name)
Во-вторых, поле name
вполне себе может быть равным null
: тогда вызвать метод equals
у него нельзя. Нужна дополнительная проверка на null
:
this.name != null && this.name.equals(person.name)
Однако если name равно null
в обоих объектах Person
, имена все-таки равны.
Код четвертого сценария может выглядеть, например, так:
|
Если возрасты не равны, сразу return false
Если this.name равно null , нет смысла сравнивать через equals . Тут либо второе поле name равно null , либо нет.
Сравниваем два поля name через equals .
|
5. Метод hashCode()
Кроме метода equals
, который выполняет детальное сравнение всех полей обоих объектов, есть еще один метод, который может использоваться для неточного, но очень быстрого сравнения — hashCode()
.
Представьте, что вы сортируете в алфавитном порядке список из тысяч слов, и вам нужно постоянно попарно сравнивать слова. А слова длинные, и букв в них много. В общем, такое сравнение будет идти очень долго.
Однако его можно ускорить. Допустим, слова начинаются на разные буквы: сразу же понятно, что они разные. Вот если они начинаются на одинаковые буквы, тогда гарантий нет: в дальнейшем слова могут оказаться и равными, и различными.
Метод hashCode()
работает по похожему принципу. Если его вызвать у объекта, то он вернет некое число — аналог первой буквы в слове. Это число обладает такими свойствами:
- У одинаковых объектов всегда одинаковые hash-code
- У разных объектов могут быть одинаковые hash-code, а могут быть разные
- Если у объектов разные hash-code, объекты точно разные
Для большего понимания перепишем эти свойства относительно слов:
- У одинаковых слов всегда одинаковые первые буквы
- У разных слов могут быть одинаковые первые буквы, а могут быть и разные
- Если у слов разные первые буквы, слова точно разные
Последнее свойство и используется для ускоренного сравнения объектов:
Сначала у двух объектов вычисляются hash-code. Если эти hash-code разные, то объекты точно разные, и сравнивать их дальше не нужно.
А вот если hash-code одинаковые, придется все же сравнивать объекты с помощью equals.
6. Контракты в коде
Описанное выше поведение должны реализовывать все классы в Java. Проверить правильность сравнения объектов на уровне компиляции никак нельзя.
Все Java-программисты договорились, что если они пишут свою реализацию метода equals() вместо стандартной (из класса Object
), они также должны написать свою реализацию метода hashCode()
, чтобы озвученные выше правила сохранялись.
Такая договоренность называется контрактом.
Если вы добавляете в свой класс только реализацию одного метода equals()
или только hashCode()
, вы грубо нарушаете контракт (нарушаете договоренность). Так делать нельзя.
Если другие программисты будут использовать ваш код, он может работать неправильно. Более того, вы тоже будете использовать код, который работает на основе вышеозвученных контрактов.
Все коллекции в Java при поиске элемента внутри коллекции сначала сравнивают hash-code объектов, а только потом вызывают для сравнения метод equals
.
Поэтому если вы напишете свой класс, а в нем новую функцию equals
, но не напишете метод hashCode()
или реализуете его с ошибками, коллекции могут неправильно работать с вашими объектами.
Например, вы добавили объект в список, затем ищете его с помощью метода contains()
, а коллекция ваш объект не находит.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ