1. Сравнение объектов в Java

Объекты в Java можно сравнивать как по ссылке, так и по значению.

Сравнение ссылок

Если две переменные указывают на один и тот же объект в памяти, то ссылки, которые хранятся в этих переменных равны. Если сравнить такие переменные с помощью оператора равенства ==, вы получите true, что логично. Тут все просто.

Код Вывод на экран
Integer a = 5;
Integer b = a;
System.out.println(a == b);


true

Сравнение по значению

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

Для определения идентичности разных объектов нужно использовать метод equals() Пример:

Код Вывод на экран
String a = new String("Привет");
String b = new String("Привет");
System.out.println(a == b);
System.out.println(a.equals(b));


false
true

Метод equals есть не только у класса String — он есть у вообще всех классов.

Даже у тех, которые вы только будете писать, и вот почему.


undefined
12
Задача
Java Syntax Pro, 12 уровень, 4 лекция
Недоступна
Двигатель — сердце автомобиля
Двигатель — сложный механизм, для описания которого недостаточно обычной переменной. В классе Car создай внутренний класс (inner class) Engine. В классе Engine создай boolean поле isRunning, которое принимает значение true, если двигатель запущен, и false в противном случае. Также в этот класс добав

2. Класс Object

Все классы в Java считаются унаследованными от класса Object. Это создатели Java так придумали.

А если некий класс унаследован от класса Object, в этом классе-наследнике появляются все методы класса Object. Это и есть главный эффект наследования.

Другими словами, у каждого класса, даже если это не написано в его коде, есть все методы, которые есть у класса Object.

А среди таких методов есть методы, которые имеют отношение к сравнению объектов. Это метод equals() и метод hashCode().

Код Как будет на самом деле:
class Person
{
   String name;
   int age;
}
class Person extends Object
{
   String name;
   int age;

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

   public int hashCode()
   {
      return адрес_объекта_в_памяти;         //это дефолтная реализация, но может быть и другая
   }
}

В примере выше мы создали простой класс Person с параметрами name и age, без единого метода. Однако, т.к. все классы считаются унаследованными от класса Object, у класса Person скрытно появились два метода:

Метод Описание
boolean equals(Object obj)
Сравнивает текущий объект и переданный объект
int hashCode()
Возвращает hash-code текущего объекта

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

Код Вывод на экран
Integer a = 5;
String s = "Привет";
System.out.println(a.equals(s));
System.out.println(s.equals(a));


false
false
Object a = new Integer(5);
Object b = new Integer(5);
System.out.println(a.equals(b)) ;


true

3. Метод equals()

Унаследованный от класса Object метод equals() содержит самый простой алгоритм сравнивания текущего и переданного объектов — он просто сравнивает их ссылки.

Тот же эффект вы получите, если просто сравните переменные класса Person вместо вызова метода equals(). Пример:

Код Вывод на экран
Person a = new Person();
a.name = "Аня";

Person b = new Person();
b.name = "Аня";

System.out.println(a == b);
System.out.println(a.equals(b));






false
false

Метод equals просто сравнивает у себя внутри ссылки a и b.

Однако у класса String сравнение работает по-другому. Почему?

Потому что разработчики класса String написали собственную реализацию метода equals().

Реализация метода equals()

Давайте и мы напишем свою реализацию метода equals в классе Person. Разберем 4 основных случая.

Важно:
Вне зависимости от того, для какого класса переопределять метод equals, он всегда принимает параметр типа Object

Сценарий 1: в метод equals передали тот же самый объект, у которого вызвали метод equals. Если ссылки у текущего и переданного объектов равны, нужно вернуть true. Объект совпадает сам с собой.

В коде это будет выглядеть так:

Код Описание
public boolean equals(Object obj)
{
   if (this == obj)
    return true;

   остальной код метода equals
}


Сравниваем ссылки

Сценарий 2: в метод equals передали ссылку null — сравнивать не с чем. Объект, у которого вызвали метод equals, точно не null, значит, в этом случае нужно вернуть false.

В коде это будет выглядеть так:

Код Описание
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   остальной код метода equals
}


Сравниваем ссылки


Переданный объект — null?

Сценарий 3: в метод equals передали ссылку на объект вообще не класса Person. Равен ли объект класса Person объекту класса не-Person? Тут уже решает сам разработчик класса Person — как хочет, так и сделает.

Но обычно все же объекты считаются равными, если это объекты одного класса. Поэтому если в наш метод equals передали объект не класса Person, мы будем всегда возвращать false. А как проверить, какого типа объект? Правильно: с помощью оператора instanceof.

Вот как будет выглядеть наш новый код:

Код Описание
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   if (!(obj instanceof Person))
      return false;

   остальной код метода equals
}


Сравниваем ссылки


Переданный объект — null?


Если переданный объект не типа Person

4. Сравнение двух объектов Person

Что мы получили в итоге? Если мы дошли до конца метода, значит, у нас объект типа Person и ссылка не null. Тогда преобразовываем его к типу Person и будем сравнивать внутренности обоих объектов. Это и есть наш сценарий номер 4.

Код Описание
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   if (!(obj instanceof Person))
      return false;

   Person person = (Person) obj;

   остальной код метода equals
}


Сравниваем ссылки


Переданный объект — null?


Если переданный объект не типа Person


Операция приведения типа

А как сравнивать два объекта Person? Они равны, если у них равны имена (name) и возраст (age). Итоговый код будет выглядеть так:

Код Описание
public boolean equals(Object obj)
{
   if (this == obj)
      return true;

   if (obj == null)
      return false;

   if (!(obj instanceof Person))
      return false;

   Person person = (Person) obj;

   return this.name == person.name && this.age == person.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, имена все-таки равны.

Код четвертого сценария может выглядеть, например, так:

Person person = (Person) obj;

if (this.age != person.age)
   return false;

if (this.name == null)
   return person.name == null;

return this.name.equals(person.name);


Если возрасты не равны,
сразу return false

Если this.name равно null, нет смысла сравнивать через equals. Тут либо второе поле name равно null, либо нет.

Сравниваем два поля name через equals.

undefined
12
Задача
Java Syntax Pro, 12 уровень, 4 лекция
Недоступна
Утильный калькулятор
Давай проведем рефакторинг кода: вынесем отдельно утильные методы. Для этого создай внутренний статический (вложенный) класс Calculator и перенеси в него методы add, subtract, multiply и divide. Не забудь подкорректировать их вызовы.

5. Метод hashCode()

Кроме метода equals, который выполняет детальное сравнение всех полей обоих объектов, есть еще один метод, который может использоваться для неточного, но очень быстрого сравнения — hashCode().

Представьте, что вы сортируете в алфавитном порядке список из тысяч слов, и вам нужно постоянно попарно сравнивать слова. А слова длинные, и букв в них много. В общем, такое сравнение будет идти очень долго.

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

Метод hashCode() работает по похожему принципу. Если его вызвать у объекта, то он вернет некое число — аналог первой буквы в слове. Это число обладает такими свойствами:

  • У одинаковых объектов всегда одинаковые hash-code
  • У разных объектов могут быть одинаковые hash-code, а могут быть разные
  • Если у объектов разные hash-code, объекты точно разные

Для большего понимания перепишем эти свойства относительно слов:

  • У одинаковых слов всегда одинаковые первые буквы
  • У разных слов могут быть одинаковые первые буквы, а могут быть и разные
  • Если у слов разные первые буквы, слова точно разные

Последнее свойство и используется для ускоренного сравнения объектов:

Сначала у двух объектов вычисляются hash-code. Если эти hash-code разные, то объекты точно разные, и сравнивать их дальше не нужно.

А вот если hash-code одинаковые, придется все же сравнивать объекты с помощью equals.


undefined
12
Задача
Java Syntax Pro, 12 уровень, 4 лекция
Недоступна
Объекты внутренних и вложенных классов
В классе Outer есть внутренний (Inner) и вложенный (Nested) классы. В методе main класса Solution создай по одному объекту каждого из них.

6. Контракты в коде

Описанное выше поведение должны реализовывать все классы в Java. Проверить правильность сравнения объектов на уровне компиляции никак нельзя.

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

Такая договоренность называется контрактом.

Если вы добавляете в свой класс только реализацию одного метода equals() или только hashCode(), вы грубо нарушаете контракт (нарушаете договоренность). Так делать нельзя.

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

Важно!

Все коллекции в Java при поиске элемента внутри коллекции сначала сравнивают hash-code объектов, а только потом вызывают для сравнения метод equals.

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

Например, вы добавили объект в список, затем ищете его с помощью метода contains(), а коллекция ваш объект не находит.