Методы объектов Java: equals (Object)

Введение Эта статья является продолжением серии статей, описывающих часто забываемые методы базового класса Object языка Java. Ниже приведены методы базового объекта Java, которые присутствуют во всех объектах Java из-за неявного наследования объекта. * toString [/ javas-object-methods-tostring /] * getClass [/ javas-object-methods-getclass /] * равно (вы здесь) * hashCode [https://stackabuse.com/javas-object-methods- хэш-код] * клон [/ javas-object-m

Вступление

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

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

== vs равно (Объект)

Как вы уже догадались, метод equals(Object) используется для проверки равенства ссылочных типов (объектов) в Java. Хорошо, имеет смысл, но вы также можете подумать: "Почему я не могу просто использовать == ?" Ответ на этот вопрос заключается в том, что когда дело доходит до ссылочных типов, == истинен только при сравнении двух ссылок на один и тот же экземпляр объекта в памяти. С другой стороны, equals(Object) можно переопределить, чтобы реализовать понятие логической эквивалентности, а не просто эквивалентности экземпляров .

Я думаю, что пример лучше всего описывает эту разницу между использованием стиха == equals(Object) для строк.

 public class Main { 
 public static void main(String[] args) { 
 String myName = "Adam"; 
 String myName2 = myName; // references myName 
 String myName3 = new String("Adam"); // new instance but same content 
 
 if (myName == myName2) 
 System.out.println("Instance equivalence: " + myName + " & " + myName2); 
 
 if (myName.equals(myName2)) 
 System.out.println("Logical equivalence: " + myName + " & " + myName2); 
 
 if (myName == myName3) 
 System.out.println("Instance equivalence: " + myName + " & " + myName3); 
 
 if (myName.equals(myName3)) 
 System.out.println("Logical equivalence: " + myName + " & " + myName3); 
 } 
 } 

Выход:

 Instance equivalence: Adam & Adam 
 Logical equivalence: Adam & Adam 
 Logical equivalence: Adam & Adam 

В приведенном выше примере я создал и сравнил три строковые переменные: myName , myName2 который является копией ссылки на myName , и myName3 который является полностью новым экземпляром, но с тем же содержимым. Сначала я показываю, что оператор == myName и myName2 как эквивалент экземпляра, чего я ожидал, потому что myName2 - это просто копия ссылки. Из-за того, что myName и myName2 являются идентичными ссылками на экземпляры, следует, что они должны быть логически эквивалентны.

Последние два сравнения действительно демонстрируют разницу между использованием == и equals(Object) . Сравнение экземпляров с использованием == демонстрирует, что это разные экземпляры с их собственными уникальными ячейками памяти, в то время как логическое сравнение с использованием equals(Object) показывает, что они содержат точно такое же содержимое.

Погружение в равных (объект)

Хорошо, теперь мы знаем разницу между == и equals(Object) , но что, если бы я сказал вам, что базовая реализация класса Object фактически дает тот же результат, что и оператор ==

Какие...!? Я знаю ... это кажется странным, но разработчики Java должны были с чего-то начинать. Позвольте мне сказать, что по умолчанию метод equals(Object) который вы наследуете в своих пользовательских классах, просто проверяет равенство экземпляров. Мы, как разработчики, должны определить, уместно это или нет, то есть определить, существует ли понятие логической эквивалентности, которое требуется для нашего класса.

Опять же, позвольте мне использовать Person который я представил ранее в этой серии, для большей демонстрации.

 public class Person { 
 private String firstName; 
 private String lastName; 
 private LocalDate dob; 
 
 public Person(String firstName, String lastName, LocalDate dob) { 
 this.firstName = firstName; 
 this.lastName = lastName; 
 this.dob = dob; 
 } 
 
 // omitting getters and setters for brevity 
 
 @Override 
 public String toString() { 
 return "<Person: firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">"; 
 } 
 } 

Позвольте мне снова использовать простую программу, заключенную в Main которая демонстрирует как идентичное равенство экземпляров, так и логическое равенство путем переопределения equals(Object) .

 import java.time.LocalDate; 
 
 public class Main { 
 public static void main(String[] args) { 
 Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23")); 
 Person me2 = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23")); 
 
 if (me != me2) 
 System.out.println("Not instance equivalent"); 
 
 if (!me.equals(me2)) 
 System.out.println("Not logically equivalent"); 
 } 
 } 

Выход:

 Not instance equivalent 
 Not logically equivalent 

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

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

Правила, изложенные в документации Java по равенству для заданных экземпляров объекта x , y и z , следующие:

  • рефлексивный: x.equals(x) должен быть истинным для всех ненулевых ссылочных экземпляров x
  • симметричный: x.equals(y) и y.equals(x) должны быть истинными для всех ненулевых ссылочных экземпляров x и y
  • транзитивный: если x.equals(y) и y.equals(z) то x.equals(z) также должен быть истинным для ненулевых ссылочных экземпляров x , y и z
  • согласованность: x.equals(y) всегда должен иметь значение true, если никакие значения элементов, используемые в реализации equals, не изменились в x и y отличных от NULL.
  • нет нулевого равенства: x.equals(null) никогда не должно быть истинным
  • всегда переопределять hashCode() при переопределении equals()

Распаковка правил замены equals (Object)

A. Возвратный: x.equals (x)

Для меня это легче всего понять. Плюс реализация по умолчанию метода equals(Object) гарантирует это, но для полноты картины ниже я приведу пример реализации, который следует этому правилу:

 class Person { 
 // omitting for brevity 
 
 @Override 
 public boolean equals(Object o) { 
 if (this == o) { 
 return true; 
 } 
 return false; 
 } 
 } 

Б. Симметричный: x.equals (y) и y.equals (x)

На первый взгляд это может показаться интуитивным, но на самом деле довольно легко ошибиться и нарушить это правило. Фактически, основная причина, по которой это часто нарушается, заключается в наследовании, которое является очень популярной вещью в Java.

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

Чтобы реализовать логический тест, я хочу сравнить поля, содержащие состояние, между двумя экземплярами класса people, обозначенными как x и y . Кроме того, я должен также убедиться, что два экземпляра имеют один и тот же тип экземпляра, например:

 class Person { 
 // omitting for brevity 
 
 @Override 
 public boolean equals(Object o) { 
 if (this == o) { 
 return true; 
 } 
 if (!(o instanceof Person)) { 
 return false; 
 } 
 Person p = (Person)o; 
 return firstName.equals(p.firstName) 
 && lastName.equals(p.lastName) 
 && dob.equals(p.dob); 
 } 
 } 

Хорошо, должно быть очевидно, что Person теперь имеет гораздо более надежную реализацию equals(Object) . Теперь позвольте мне привести пример того, как наследование может вызвать нарушение симметрии. Ниже приведен, казалось бы, безобидный класс под названием Employee , наследуемый от Person .

 import java.time.LocalDate; 
 
 public class Employee extends Person { 
 
 private String department; 
 
 public Employee(String firstName, String lastName, LocalDate dob, String department) { 
 super(firstName, lastName, dob); 
 this.department = department; 
 } 
 
 @Override 
 public boolean equals(Object o) { 
 if (o == this) { 
 return true; 
 } 
 
 if (!(o instanceof Employee)) { 
 return false; 
 } 
 Employee p = (Employee)o; 
 return super.equals(o) && department.equals(p.department); 
 
 } 
 } 

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

 import java.time.LocalDate; 
 
 public class Main { 
 public static void main(String[] args) { 
 Person billy = new Person("Billy", "Bob", LocalDate.parse("2016-09-09")); 
 MinorPerson billyMinor = new MinorPerson( 
 billy.getFirstName(), 
 billy.getLastName(), 
 billy.getDob()); 
 
 System.out.println("billy.equals(billyMinor): " + billy.equals(billyMinor)); 
 System.out.println("billyMinor.equals(billy): " + billyMinor.equals(billy)); 
 } 
 } 

Выход:

 billy.equals(billyEmployee): true 
 billyEmployee.equals(billy): false 

Ой! Очевидно, что нарушение симметрии, billy равно billyEmployee но обратное неверно. Так что же мне делать? Что ж, я мог бы сделать что-то вроде следующего, учитывая, что я написал код и знаю, что наследует, а затем изменить метод Employee equals(Object) следующим образом:

 import java.time.LocalDate; 
 
 public class Employee extends Person { 
 
 private String department; 
 
 public Employee(String firstName, String lastName, LocalDate dob, String department) { 
 super(firstName, lastName, dob); 
 this.department = department; 
 } 
 
 @Override 
 public boolean equals(Object o) { 
 if (o == this) { 
 return true; 
 } 
 
 if (instanceof Person && !(o instanceof Employee)) { 
 return super.equals(o); 
 } 
 
 if (o instanceof Employee) { 
 Employee p = (Employee)o; 
 return super.equals(o) && department.equals(p.department); 
 } 
 
 return false; 
 } 
 } 

Выход:

 billy.equals(billyEmployee): true 
 billyEmployee.equals(billy): true 

Ура, у меня есть симметрия! Но я действительно в порядке? Обратите внимание на то, как я изо всех сил стараюсь заставить Employee теперь соответствовать ... это должно поднять красный флаг, который вернется, чтобы укусить меня позже, как я продемонстрирую в следующем разделе.

C. Транзитивность: если x.equals (y) и y.equals (z), то x.equals (z)

До сих пор я удостоверился, что мои классы Person и Employee equals(Object) которые являются как рефлексивными, так и симметричными, поэтому мне нужно проверить, что транзитивность также соблюдается. Я сделаю это ниже.

 import java.time.LocalDate; 
 
 public class Main { 
 public static void main(String[] args) { 
 Person billy = new Person("Billy", "Bob", LocalDate.parse("2016-09-09")); 
 Employee billyEngineer = new Employee( 
 billy.getFirstName(), 
 billy.getLastName(), 
 billy.getDob(), 
 "Engineering"); 
 Employee billyAccountant = new Employee("Billy", "Bob", LocalDate.parse("2016-09-09"), "Accounting"); 
 
 System.out.println("billyEngineer.equals(billy): " + billyEngineer.equals(billy)); 
 System.out.println("billy.equals(billyAccountant): " + billy.equals(billyAccountant)); 
 System.out.println("billyAccountant.equals(billyEngineer): " + billyAccountant.equals(billyEngineer)); 
 } 
 } 

Выход:

 billyEngineer.equals(billy): true 
 billy.equals(billyAccountant): true 
 billyAccountant.equals(billyEngineer): false 

Штопать! Какое-то время я шел по такому хорошему пути. Что случилось? Что ж, оказывается, что при классическом наследовании в языке Java вы не можете добавить идентифицирующий член класса к подклассу и все же ожидать, что сможете переопределить equals(Object) без нарушения симметрии или транзитивности. Лучшая альтернатива, которую я нашел, - использовать шаблоны композиции вместо наследования. Это эффективно нарушает жесткую иерархию наследования между классами, например:

 import java.time.LocalDate; 
 
 public class GoodEmployee { 
 
 private Person person; 
 private String department; 
 
 public GoodEmployee(String firstName, String lastName, LocalDate dob, String department) { 
 person = new Person(firstName, lastName, dob); 
 this.department = department; 
 } 
 
 @Override 
 public boolean equals(Object o) { 
 if (o == this) { 
 return true; 
 } 
 
 if (!(o instanceof Employee)) { 
 return false; 
 } 
 
 GoodEmployee p = (GoodEmployee)o; 
 return person.equals(o) && department.equals(p.department); 
 } 
 } 

D. Согласованность: x.equals (y), пока ничего не меняется

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

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

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

 import java.time.LocalDate; 
 
 public class Person { 
 private final String firstName; 
 private final String lastName; 
 private final LocalDate dob; 
 
 public Person(String firstName, String lastName, LocalDate dob) { 
 this.firstName = firstName; 
 this.lastName = lastName; 
 this.dob = dob; 
 } 
 
 public String getFirstName() { 
 return firstName; 
 } 
 
 public String getLastName() { 
 return lastName; 
 } 
 
 public LocalDate getDob() { 
 return dob; 
 } 
 
 @Override 
 public String toString() { 
 Class c = getClass(); 
 return "<" + c.getSimpleName() + ": firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">"; 
 } 
 
 @Override 
 public boolean equals(Object o) { 
 if (this == o) { 
 return true; 
 } 
 if (!(o instanceof Person)) { 
 return false; 
 } 
 Person p = (Person)o; 
 return firstName.equals(p.firstName) 
 && lastName.equals(p.lastName) 
 && dob.equals(p.dob); 
 } 
 } 

E. Отсутствие нулевого равенства: x.equals (null)

Иногда вы увидите, что это принудительно выполняется посредством прямой проверки того, что экземпляр Object o равен null , но в приведенном выше примере это неявно проверяется с помощью !(o instanceof Person) из-за того, что команда instanceof всегда будет возвращать false, если левый операнд равен нулю.

F. Всегда переопределяйте hashCode() при переопределении equals(Object)

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

Заключение

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

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

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus