Время на прочтение
8 мин
Количество просмотров 17K
Вступление
Сравнение объектов является важной функцией объектно-ориентированных языков программирования. В этом руководстве мы рассмотрим некоторые функции языка Java, которые позволяют нам сравнивать объекты. Также мы обратим внимание на подобные функции во внешних библиотеках.
Операторы == и !=
Давайте начем с операторов ==
и !=
, которые могут сказать, одинаковы ли Java-объекты или нет соответственно.
Примитивы
Для примитивных типов быть одинаковым означает иметь одинаковые значения:
assertThat(1 == 1).isTrue();
Благодаря автоматической распаковке это также работает при сравнении примитивного значения с его классом-оберткой:
Integer a = new Integer(1);
assertThat(1 == a).isTrue();
Если две целочисленные переменные имеют разные значения, оператор ==
вернет false
, а оператор !=
вернет true
.
Объекты
Предположим, мы хотим сравнить два экземпляра классов-оберток Integer с одинаковыми значениями:
Integer a = new Integer(1);
Integer b = new Integer(1);
assertThat(a == b).isFalse();
При сравнении двух объектов их значения не равняются 1. Скорее различаются их адреса памяти в стеке, поскольку оба объекта создаются с использованием оператора new
. Если мы присвоим переменной а
переменную b
, то получим другой результат:
Integer a = new Integer(1);
Integer b = a;
assertThat(a == b).isTrue();
Теперь давайте посмотрим, что происходит, когда мы используем фабричный метод Integer#valueOf
:
Integer a = Integer.valueOf(1);
Integer b = Integer.valueOf(1);
assertThat(a == b).isTrue();
В этом случае они считаются одинаковыми. Это связано с тем, что метод valueOf()
держит Integer в кеше, чтобы избежать создания слишком большого количества оберток с одинаковыми значениями. Поэтому метод возвращает один и тот же экземпляр Integer для обоих вызовов.
Java также делает это для переменных типа String:
assertThat("Hello!" == "Hello!").isTrue();
Однако, если они созданы с помощью оператора new
, они не будут одинаковыми.
Наконец, две нулевые ссылки считаются одинаковыми, в то время как любой не-null объект считается отличным от null:
assertThat(null == null).isTrue();
assertThat("Hello!" == null).isFalse();
Конечно, поведение операторов равенства может быть ограничивающим. Что если мы хотим сравнить два объекта, расположенных по разным адресам и при этом полагать, что они равны на основании их внутренних состояний? Как это сделать, мы увидим в следующих пунктах.
Метод Object#equals
Теперь давайте поговорим о более широком концепте равенства с методом equals()
.
Этот метод определен в классе Object
, поэтому каждый объект Java наследует его. По умолчанию его реализация сравнивает адреса памяти объектов, поэтому он работает так же, как и оператор ==
. Однако мы можем переопределить этот метод, чтобы определить, что для наших объектов означает равенство внутренних состояний.
Во-первых, давайте посмотрим, как он ведет себя для существующих объектов, таких как Integer:
Integer a = new Integer(1);
Integer b = new Integer(1);
assertThat(a.equals(b)).isTrue();
Наш метод по-прежнему возвращает true, когда оба объекта одинаковы.
Отметим здесь, что мы можем передать объект null в качестве аргумента метода, но не в качестве объекта, для которого мы вызываем метод.
Мы также можем использовать метод equals()
с любым пользовательским объектом. Допустим, у нас есть класс Person:
public class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Мы можем переопределить метод equals()
для этого класса, чтобы мы могли сравнить два объекта Persons
на основе их внутренних данных:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person that = (Person) o;
return firstName.equals(that.firstName) &&
lastName.equals(that.lastName);
}
Чтобы узнать больше по этой теме, читайте вот эту статью.
Статический метод Objects#equals
Давайте посмотрим на статический метод Objects#equals
. Ранее мы упоминали, что нельзя использовать null в качестве значения первого объекта, иначе будет выброшено исключение NullPointerException
.
Метод equals()
вспомогательного класса Objects
решает эту проблему. Он принимает два аргумента и сравнивает их, а также обрабатывает значения null.
Давайте снова сравним объекты Person
:
Person joe = new Person("Joe", "Portman");
Person joeAgain = new Person("Joe", "Portman");
Person natalie = new Person("Natalie", "Portman");
assertThat(Objects.equals(joe, joeAgain)).isTrue();
assertThat(Objects.equals(joe, natalie)).isFalse();
Как мы объяснили, этот метод обрабатывает значения null. Следовательно, если оба аргумента равны null, он вернет значение true, а если только один из них равен null, он вернет значение false.
Это может быть очень удобно. Допустим, мы хотим добавить не строго обязательную дату рождения в наш класс Person
:
public Person(String firstName, String lastName, LocalDate birthDate) {
this(firstName, lastName);
this.birthDate = birthDate;
}
Затем нужно обновить метод equals()
, но с обработкой null значений. Мы можем сделать это, добавив условие в метод equals()
:
birthDate == null ? that.birthDate == null : birthDate.equals(that.birthDate);
Однако же, если мы добавим слишком много полей с возможными null значениями в класс, он может стать запутанным. Использование метода Objects#equals
в реализации equals()
намного чище, что улучшает читаемость кода.
Objects.equals(birthDate, that.birthDate);
Интерфейс Comparable
Логику сравнения также можно использовать для размещения объектов в определенном порядке. Интерфейс Comparable позволяет нам определять порядок между объектами, выявляя, является ли объект больше, меньше или равным другому.
Интерфейс Comparable является дженериком и имеет только один метод, compareTo()
, который принимает аргумент дженерик-типа и возвращает int. Возвращаемое значение отрицательное, если this
меньше аргумента, 0, в случае если они равны, и положительное в обратном случае.
Допустим, в классе Person
мы хотим сравнить объекты Person
по их фамилии:
public class Person implements Comparable<Person> {
//...
@Override
public int compareTo(Person o) {
return this.lastName.compareTo(o.lastName);
}
}
Метод compareTo()
вернет отрицательный int, если оно вызвано с именем Person
, имеющим большую фамилию, чем this, ноль, если ту же фамилию, и положительное значение в обратном случае.
Чтобы узнать подробнее, прочитайте статью на эту тему.
Интерфейс Comparator
Интерфейс Comparator является дженериком и содержит метод compare
, который принимает два аргумента этого типа и возвращает integer. Мы ранее уже видели этот шаблон с интерфейсом Comparable.
Comparator
аналогичен ему; однако он отделен от определения класс. Следовательно, мы можем определить столько Comparator
, сколько захотим для одного класса, где мы можем предоставить только одну реализацию Comparable
.
Представим, что у нас есть веб-страница, содержащая список людей в виде таблицы, и мы хотим предложить пользователю возможность сортировать их по именам, а не по фамилиям. Это невозможно с Comparable
, если мы также хотим сохранить нашу текущую реализацию, но мы можем реализовать наши собственные Comparators
.
Давайте создадим Person Comparator
, который будет сравнивать их только по именам:
Comparator<Person> compareByFirstNames = Comparator.comparing(Person::getFirstName);
Теперь отсортируем список людей, используя Comparator
:
Person joe = new Person("Joe", "Portman");
Person allan = new Person("Allan", "Dale");
List<Person> people = new ArrayList<>();
people.add(joe);
people.add(allan);
people.sort(compareByFirstNames);
assertThat(people).containsExactly(allan, joe);
В интерфейсе Comparator
есть и другие методы, которые мы можем использовать в реализации compareTo()
:
@Override
public int compareTo(Person o) {
return Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparing(Person::getBirthDate, Comparator.nullsLast(Comparator.naturalOrder()))
.compare(this, o);
}
В этом примере мы сначала сравниваем фамилии, а затем имена. Затем мы сравниваем даты рождения, но, поскольку они нулевые, нужно рассказать, что с этим делать. Для этого мы задаем второй аргумент, чтобы сказать, что их нужно сравнить в соответствии с их естественным порядком, причем значения null идут последними.
Apache Commons
Давайте взглянем на библиотеку Apache Commons. Прежде всего, импортируем зависимость Maven.
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
Метод ObjectUtils#notEqual
Для начала поговорим о методе ObjectUtils#notEqual
. Потребуется два аргумента Object
, чтобы определить, не равны ли они, в соответствии с их собственной реализацией метода equals()
. Он также обрабатывает значения null.
Используем наши примеры со String повторно:
String a = new String("Hello!");
String b = new String("Hello World!");
assertThat(ObjectUtils.notEqual(a, b)).isTrue();
Следует отметить, что у ObjectUtils
есть метод equals()
, что однако является устаревшим с момента возникновения Java 7, когда появились Objects#equals
.
Метод ObjectUtils#compare
Теперь давайте сравним порядок объектов с помощью метода ObjectUtils#compare
. Это дженерик-метод, который принимает два аргумента Comparable
дженерик типа и возвращает Integer.
Приведем пример, снова используя Strings:
String first = new String("Hello!");
String second = new String("How are you?");
assertThat(ObjectUtils.compare(first, second)).isNegative();
По умолчанию этот метод обрабатывает значения null, считая их бОльшими. Он также предлагает перегруженную версию с аргументом boolean, которая предлагает изменить это поведение и считать их мЕньшими, принимая аргумент boolean.
Guava
Давайте взглянем на Guava. Прежде всего импортируем зависимость:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
Метод Objects#equal
Как и библиотека Apache Commons, Google предоставляет нам метод определения равенства двух объектов, Objects#equal
. Хотя у них разные реализации, возвращают они одинаковые результаты:
String a = new String("Hello!");
String b = new String("Hello!");
assertThat(Objects.equal(a, b)).isTrue();
Хотя он не помечен как устаревший, в JavaDoc говорится, что этот метод следует считать устаревшим, поскольку Java 7 предоставляет метод Objects#equals
.
Методы сравнения
Библиотека Guava не предлагает метода для сравнения двух объектов (в следующем разделе мы увидим, что мы можем для этого сделать), но она предоставляет нам методы для сравнения примитивных значений. Давайте возьмем вспомогательный класс Ints
и посмотрим, как работает его метод compare()
:
assertThat(Ints.compare(1, 2)).isNegative();
Как обычно, он возвращает integer, который может быть отрицательным, нулевым или положительным, если первый аргумент меньше, равен или больше второго соответственно. Подобные методы существуют для всех примитивных типов, за исключением bytes.
Класс ComparisonChain
Наконец, библиотека Guava предлагает класс ComparisonChain
, который позволяет нам сравнивать два объекта по цепочке сравнений. Мы можем легко сравнить два объекта Person
по имени и фамилии:
Person natalie = new Person("Natalie", "Portman");
Person joe = new Person("Joe", "Portman");
int comparisonResult = ComparisonChain.start()
.compare(natalie.getLastName(), joe.getLastName())
.compare(natalie.getFirstName(), joe.getFirstName())
.result();
assertThat(comparisonResult).isPositive();
Базовое сравнение достигается с помощью метода compareTo()
, поэтому аргументы, передаваемые методам compare()
, должны быть либо примитивными, либо Comparable.
Заключение
В этой статье мы узнали о разных способах сравнения объектов в Java. Мы изучили разницу между тождественностью, равенством и порядком. Мы также рассмотрели соответствующие функции в библиотеках Apache Commons и Guava.
Полный код из статьи можно найти на GitHub.
Приглашаем всех желающих на открытое занятие «Языки статической и динамической типизации». На этом вебинаре поговорим о стилях программирования и необходимости каждого из них. Разберём основные принципы объектно-ориентированного стиля (Инкапсуляция, Наследование, Полиморфизм). Рассмотрим возможности функционального стиля, которые предоставляет язык Java. Регистрация на вебинар.
Прошу пояснить код. В частности, вот эту строку
if (!super.equals(other)) return false;
Основной вопрос состоит в следующем. Как Вы думаете, что делает эта строчка процитированного кода? Да, там вызывается метод equals родительского класса. А кто у нас у класса в данном случае родитель? Если верить спецификации, то это java.lang.Object. Значит сначала мы в первой строке Вашего кода вызываем метод equals класса Object. Как Вы думаете, что он делает? Вроде как это известная информация, но для большей достоверности вот цитата из исходников
public boolean equals(Object obj) {
return (this == obj);
}
То есть он возвращает true только в том случае, когда у нас ссылки на объект совпадают. Если же у нас 2 отдельных объекта, которые хранятся в памяти в разных местах (пусть у них содержимое одинаковое), ссылки на них не совпадают! А что это значит? Это значит, что благодаря строке if(!super.equals(other)) return false;
Ваш метод equals
будет возвращать true
, только если мы будем сравнивать объект сам с собой! Наверное, это не то, чего бы всем хотелось, не так ли?
Думаю, эту строку надо убрать и тогда это будет больше похоже на правду. Но вообще каждый раз при написании своего equals надо принимать во внимание тот факт, что когда-то создатели Java
и класса java.lang.Object
в частности написали условия, которым должен удовлетворять переопределённый метод equals, найти их можно в исходниках, либо здесь.
И ещё заметка относительно hashCode
. Вы упомянули только одно из требований котракта по hashCode
: если 2 объекта считаются равными (equals возвращает true), то hashCode для них должен возвращать одинаковое значение. Дальше
Обратное утверждение не верно.
на самом деле в контракте, найти можно — здесь стоит продолжение: «However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hashtables». То есть надо принимать этот факт во внимание, как и ещё один пункт из этого же контракта.
Методы equals и hashCode в Java в чём-то очень схожи, и даже вместе генерируются средствами IDE, таких как IntelliJ IDEA. Что в них общего, каковы отличия, и что будет, если использовать только один из методов?
- Метод equals()
- Контракт equals()
- Использование equals
- Метод hashcode()
- Контракт hashCode()
- Использование hashCode
- Почему equals и hashCode в Java переопределяются вместе
Как вы наверняка знаете, сравнение посредством ==
в Java сравнивает ссылки, но объекты таким образом не сравнить. Следующий пример подобного сравнения двух строк вернёт false
:
public static void main(String[] args) {
//false
System.out.println(new String("Tproger") == new String("Tproger"));
}
Пусть значения и одинаковы, но переменные String
указывают на разные объекты.
Тут-то в игру и вступает метод equals()
, предусмотренный в Java для сравнения именно объектов. Данный метод проверяет два объекта одного происхождения на логическую равность.
То есть, сравнивая два объекта, программисту необходимо понять, эквивалентны ли их поля. При этом необязательно все поля должны быть идентичными, поскольку метод equals()
подразумевает именно логическое равенство.
Контракт equals() в Java
Используя equals
, мы должны придерживаться основных правил, определённых в спецификации Java:
- Рефлексивность —
x.equals(x)
возвращаетtrue
. - Симметричность —
x.equals(y) <=> y.equals(x)
. - Транзитивность —
x.equals(y) <=> y.equals(z) <=> x.equals(z)
. - Согласованность — повторный вызов
x.equals(y)
должен возвращать значение предыдущего вызова, если сравниваемые поля не изменялись. - Сравнение null —
x.equals(null)
возвращаетfalse
.
Использование equals
Предположим, у нас есть класс Programmer
, в котором предусмотрены поля с должность и зарплатой:
public class Programmer {
private final String position;
private final int salary;
protected Programmer(String position, int salary) {
this.position = position;
this.salary = salary;
}
}
В переопределённом методе equals()
обе переменные участвуют в проверке. Также вы всегда можете убрать ту переменную, которую не хотите проверять на равенство.
Зачастую метод equals в Java определяется вместе с hashCode, но здесь мы рассмотрим первый метод отдельно:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Programmer that = (Programmer) o;
if (salary != that.salary) return false;
return Objects.equals(position, that.position);
}
Определим объекты programmer1
и programmer2
типа Programmer
с одинаковыми значениями. При их сравнении с помощью ==
вернётся false
, так как это разные объекты. Если же мы используем для сравнения метод equals()
, вернётся true
:
public class Main extends Programmer {
protected Main(String position, int salary) {
super(position, salary);
}
public static void main(String[] args) {
Programmer programmer1 = new Programmer("Junior", 300);
Programmer programmer2 = new Programmer("Junior", 300);
//false
System.out.println(programmer1 == programmer2);
//true
System.out.println(programmer1.equals(programmer2));
}
}
А вот такой результат мы получим, если хотя бы одна переменная (обозначенное поле объекта) из метода equals()
не совпадёт:
public class Main extends Programmer {
protected Main(String position, int salary) {
super(position, salary);
}
public static void main(String[] args) {
Programmer programmer1 = new Programmer("Junior", 300);
Programmer programmer2 = new Programmer("Middle", 300);
//false
System.out.println(programmer1 == programmer2);
//false
System.out.println(programmer1.equals(programmer2));
}
}
Метод hashcode() в Java
Наконец, мы дошли до сравнения методов equals и hashCode в языке Java.
Фундаментальное отличие в том, что hashCode()
— это метод для получения уникального целочисленного номера объекта, своего рода его идентификатор. Благодаря хешу (номеру) можно, например, быстро определить местонахождение объекта в коллекции.
Это число используется в основном в хеш-таблицах, таких как HashMap
. При этом хеш-функция получения числа на основе объекта должна быть реализована таким образом, чтобы обеспечить равномерное распределение элементов по хэш-таблице. А также минимизировать возможность появления коллизий, когда по разным ключам функция вернёт одинаковое значение.
В случае Java, метод hashCode()
возвращает для любого объекта 32-битное число типа int
. Сравнить два числа между собой гораздо быстрее, чем сравнить два объекта методом equals()
, особенно если в нём используется много полей.
Контракт hashCode() в Java
- Повторный вызов
hashCode
для одного и того же объекта должен возвращать одинаковые хеш-значения, если поля объекта, участвующие в вычислении значения, не менялись. - Если
equals()
для двух объектов возвращаетtrue
,hashCode()
также должен возвращать для них одно и то же число. - При этом неравные между собой объекты могут иметь одинаковый
hashCode
.
Использование hashCode
Вернёмся к нашему классу Programmer
. По-хорошему, вместе с equals()
должен быть использован и метод hashCode():
public class Programmer {
private final String position;
private final int salary;
protected Programmer(String position, int salary) {
this.position = position;
this.salary = salary;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Programmer that = (Programmer) o;
if (salary != that.salary) return false;
return Objects.equals(position, that.position);
}
@Override
public int hashCode() {
int result = position != null ? position.hashCode() : 0;
result = 31 * result + salary;
return result;
}
}
Почему equals и hashCode в Java переопределяются вместе
Сперва производится сравнение по хешу, чтобы понять, совпадают ли объекты, а только после подключается equals
, чтобы определить, совпадают ли значения полей объекта.
Рассмотрим два сценария.
1. equals есть, hashCode нет
С точки зрения метода equals два объекта будут логически равны, но по hashCode они не будут иметь ничего общего. Таким образом, помещая некий объект в хэш-таблицу, мы рискуем не получить его обратно по ключу:
Map<Point, String> m = new HashMap<>();
m.put(new Point(1, 1), "Point A");
//pointName == null
String pointName = m.get(new Point(1, 1));
2. hashCode есть, equals нет
Метод equals
по умолчанию сравнивает указатели на объекты, определяя, ссылаются ли они на один и тот же объект. Следовательно, пример из предыдущего пункта по идее должен выполняться. Но мы по-прежнему не сможем найти наш объект в хэш-таблице.
Для успешного поиска объекта в хэш-таблице помимо сравнения хэш-значений ключа используется также определение логического равенства ключа с искомым объектом.
Читайте также об условных операторах в Java.
В этой статье мы рассмотрим операторы и методы сравнения строк в Java. Поговорим про особенности использования оператора ==, а также про методы equals(), equalsIgnoreCase и compareTo(), т. к. они используются чаще всего.
Оператор для сравнения строк «==»
В первую очередь, надо сказать, что этот оператор проверяет и сравнивает не значения, а ссылки. С его помощью вы сможете проверить, являются ли сравниваемые вами элементы одним и тем же объектом. Когда 2 переменные String указывают на тот же самый объект в памяти, сравнение вернёт true, в обратном случае — false.
В примере выше литералы интернируются компилятором, в результате чего ссылаются на один и тот же объект.
new String("Java") == "Java" // falseВышеприведённые переменные String указывают уже на различные объекты.
new String("Java") == new String("Java") // falseЗдесь тоже вышеприведенные переменные String указывают на различные объекты.
Итак, мы видим, что оператор == по сути, сравнивает не две строки, а лишь ссылки, на которые указывают строки.
class TestClass{ public static void main (String[] args){ // ссылается на тот же объект, возвращая true if( "Java" == "Java" ){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } // указывает уже на другой объект, возвращая false if(new String("Java") == "Java"){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } // указывает тоже на другой объект, возвращая false if(new String("Java") == new String("Java") ){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } } }Итог:
Statement is true Statement is false Statement is falseМетод сравнения String equals()
Сравнение строк с помощью equals позволяет проверять исходное содержимое строки. Метод возвращает true, когда параметр — объект String, представляющий собой ту же строку символов, что и объект:
Objects.equals("Java", new String("Java")) //trueКогда надо выполнить проверку, имеют ли 2 строки одинаковое значение, мы можем задействовать Objects.equals().
class TestClass{ public static void main (String[] args) { String str1 = "Java"; String str2 = "Java"; String str3 = "ASP"; String str4 = "JAVA"; String str5 = new String("Java"); // оба равны и возвращают true if(str1.equals(str2)){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } // оба не равны и возвращают false if(str1.equals(str3)){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } // оба не равны и возвращают false if(str1.equals(str4)){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } // оба равны и возвращают true if(str1.equals(str5)){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } } }Итог:
Statement is true Statement is false Statement is false Statement is trueМетод сравнения String equalsIgnoreCase()
С помощью метода equalsIgnoreCase() вы выполните сравнение строк, что называется, лексикографически, причём различия регистра будут игнорированы. Здесь значение true возвращается в том случае, если аргумент является объектом String и представляет такую же последовательность символов, что и у объекта. Прекрасное решение, если надо осуществить проверку строки на равенство, не учитывая при этом регистр.
class TestClass{ public static void main (String[] args){ String str1 = "Java"; String str2 = "JAVA"; // возвращается true, ведь обе строки равны без учёта регистра if(str1.equalsIgnoreCase(str2)){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } // возвращается false, т. к. учитывается регистр символов if(str1.equals(str2)){ System.out.println("Statement is true"); }else{ System.out.println("Statement is false"); } } }Результат
Statement is true Statement is falseМетод сравнения String compareTo()
Метод сравнения compareTo() применяется, если надо определить лексикографический порядок строк. Он выполняет сравнение значения char, действуя аналогично equals(). Когда 2 строки совпадают, compareTo() вернёт значение «ноль» (результат = 0). Сравнивая 2 строки, он вернёт положительное целое число (результат > 0), если 1-й объект String следует за 2-й строкой. Соответственно, метод вернёт отрицательный результат (результат < 0), когда 1-й объект String будет предшествовать 2-й строке:
result1 == result2 :возвращается 0; result1 > result2 :возвращается положительное значение; result1 < result2 : возвращается отрицательное значение.Приведём пример:
class TestClass{ public static void main (String[] args) { String str1 = "Java"; String str2 = "Java"; String str3 = "ASP"; int val = 0; val = str1.compareTo(str2); System.out.println(val); val = str1.compareTo(str3); System.out.println(val); val = str3.compareTo(str1); System.out.println(val); } }Итог:
На этом всё, очень надеемся, что этот материал будет вам полезен при сравнении строк в «Джава».
При подготовке статьи использовалась публикация «String Comparison in Java».
Хотите знать больше? Приходите на курс!
Implementing equals()/hashCode() and toString()
Why equals()/hashCode() and toString() are important
Those methods are used by the data structures (list, map) to implement operations
like add(), contains(), etc. So most of the data structure doesn’t work
correctly if equals()
/hashCode()
and toString()
are not correctly written
on the element.
By example, ArraysList.contains(value)
uses value.equals()
,
HashMap.get(key)
uses both key.hashCode()
and key.equals()
.
Default implementations from java.lang.Object
Object defines several methods and provide a default implementation for them
boolean equals(Object)
test if two objects are equals (same type and same values), but
the default implementation do an ==, so only check if the two objects are at
the same address in memoryint hashCode()
return a summary of the content of the object as an int
the default implementation choose a random number when the object is createdString toString()
return a textual representation of the object
the default implementation return a concatenation of the
So the default implementations only ensure that an object is equals to itself.
Writing your own equals()/hashCode()
equals()
must be valid for any object and returns false if it’s not the right type
so it starts with aninstanceof
and callsequals()
if the field value
is a reference.hashCode()
delegates to the hashCode of the field value
class User { private final String name; public User(String name) { this.name = Objects.requireNonNull(name); } public boolean equals(Object o) { return o instanceof User user && name.equals(user.name); } public int hashCode() { return name.hashCode(); } public String toString() { return "User " + name; } } var user1 = new User("Bob"); var user2 = new User("Bob"); System.out.println(user1.equals(user2)); System.out.println(user1.hashCode() == user2.hashCode()); System.out.println(user1);
With two fields
equals()
, it’s better to first check the primitive fields because a primitive check
is usually faster than a call toequals()
.hashCode()
can use the exclusive or^
to mix the hash code.
class User { private final String name; private final int age; public User(String name, int age) { this.name = Objects.requireNonNull(name); this.age = age; } public boolean equals(Object o) { return o instanceof User user && age == user.age && name.equals(user.name); } public int hashCode() { return name.hashCode() ^ age; } public String toString() { return "User " + name + " " + age; } } var user1 = new User("Bob", 31); var user2 = new User("Bob", 31); System.out.println(user1.equals(user2)); System.out.println(user1.hashCode() == user2.hashCode()); System.out.println(user1);
With several fields
- equals(), as said in chapter ‘basic_types’, array.equals() doesn’t work,
Arrays.equals() should be used instead - hashCode(),
Object.hash
compute the hash of several values separated by commas.
class User { private final String name; private final int age; private final String login; private final char[] password; public User(String name, int age, String login, char[] password) { this.name = Objects.requireNonNull(name); this.age = age; this.login = Objects.requireNonNull(login); this.password = password.clone(); } public boolean equals(Object o) { return o instanceof User user && age == user.age && name.equals(user.name) && login.equals(user.login) && Arrays.equals(password, user.password); } public int hashCode() { return Objects.hash(name, age, login, Arrays.hashCode(password)); } public String toString() { return "User " + name + " " + age + " " + login + " " + "*".repeat(password.length); } } var user1 = new User("Bob", 31, "bob", "df15cb4e019ec2eac654fb2e486c56df285c8c7b".toCharArray()); var user2 = new User("Bob", 31, "bob", "df15cb4e019ec2eac654fb2e486c56df285c8c7b".toCharArray()); System.out.println(user1.equals(user2)); System.out.println(user1.hashCode() == user2.hashCode()); System.out.println(user1);
Record implementation
For a record, the methods equals()
/hashCode()
and toString()
are already provided
so usually you don’t have to provide a new implementation.
record User(String name, int age) { public User { Objects.requireNonNull(name); } // the compiler automatically adds equals/hashCode/toString ! } var user1 = new User("Bob", 31); var user2 = new User("Bob", 31); System.out.println(user1.equals(user2)); System.out.println(user1.hashCode() == user2.hashCode()); System.out.println(user1);
В языке Java все классы наследуются от класса “java.lang.Object”. Среди наследуемых методов есть метод “equals”. Правильная реализация этого метода имеет решающее значение для корректности программы и – вопреки видимости – не обязательно тривиальна. Многие структуры данных зависят от их правильной реализации. Следовательно, неправильная его реализация приводит к их некорректному поведению.
Содержание
- 1 Метод java.lang.Object.equals
- 2 Контракты против equals
- 3 Правильная реализация метода equals
- 4 А как насчет этой проходимости?
- 5 Резюме
Метод java.lang.Object.equals
Метод equals позволяет определить, равны ли два объекта.
Его определение по умолчанию, предоставляемое классом Object, основано на объектных ссылках.
Во многих случаях такой реализации достаточно. В общем, для классов, целью которых является предоставление некоторой функциональности, лучше не реализовывать метод equals. Примером может служить HTTP-клиент.
Трудно представить, как можно сравнивать такие объекты иначе, чем через тождество. В таких случаях следует полагаться на реализацию по умолчанию.
Ситуация выглядит иначе в случае объектов, представляющих сущности из моделируемого мира, например, объектов, представляющих книги, заметки и т.д. Именно для таких типов объектов обычно предусмотрен метод equals
Правильность реализации титульного метода можно рассматривать двумя способами:
- Объекты должны быть равны, если они были бы равны в моделируемом мире. Например, две книги могут считаться одинаковыми, если они имеют одинаковый номер ISBN.
- Метод equals должен удовлетворять так называемым контрактам, которые требуются стандартом Java и соблюдение которых необходимо для правильного поведения некоторых структур данных.
Прежде чем перейти к анализу вышеупомянутых контрактов, давайте взглянем на простую иерархию классов, к которой мы будем обращаться далее.
Code language: JavaScript (javascript)
class Book { String isbn; } class Ebook extends Book { String format; }
Стандарт языка требует от равноправных реализаций поддерживать следующие инварианты:
- маневренность, то есть объект равен самому себе. Другими словами, для каждого объекта o верно, что o.equals(o) == true
- симметрия, т.е. если первый объект равен второму, то второй также равен первому. Это означает, что если o1.equals(o2) возвращает true (false), то o2.equals(o1) также должно возвращать true (false),
- согласованность, то есть для любых двух объектов метод o1.equals(o2) должен всегда возвращать одно и то же значение, если в объектах не произошло никаких изменений,
- transitive – это условие, которое гарантирует, что результат операции equals является транзитивным, т.е. если у нас есть три объекта o1, o2, o3, и если o1 равен o2, а o2 равен o3, то o1 равен o3,
- Сравнение объекта с нулевым значением всегда возвращает false.
Правильная реализация метода equals
Давайте теперь сосредоточимся на правильной реализации метода equals, то есть на той, которая сохраняет все ограничения, введенные стандартом языка.
В целом, если мы рассматриваем сравнение объектов совершенно одинакового типа, ситуация проста, и стандартные реализации, основанные на сравнении полей объектов, являются правильными и достаточными.
Однако ситуация не так проста, поскольку equals принимает в качестве аргумента параметр типа Object:
Code language: JavaScript (javascript)
public boolean equals(Object o)
Следовательно, объект любого другого типа можно сравнить с экземпляром нашего класса. И, хотя очевидно, что объекты из разных иерархий классов просто разные, равенство объектов, остающихся в одной иерархии, уже может быть соображением.
Давайте теперь сосредоточимся на классах, показанных выше, – книге и электронной книге. Давайте определим, что мы хотели бы получить ситуацию, когда электронная книга и книга могут быть равны. Рассмотрим простую реализацию:
Code language: JavaScript (javascript)
class Book { public boolean equals(Object o){ if(!(o instanceof Book)) { return false; } return this.isbn == ((Book)o).isbn; } }
Эта реализация признает, что две книги (и их производные, электронные книги) равны, если их номера ISBN одинаковы. Разумная реализация для класса Ebook может выглядеть следующим образом:
Code language: JavaScript (javascript)
class Ebook { public boolean equals(Object o) { if ((o instanceof Ebook)) { return format.equals(((Ebook) o).format) && super.equals(o); } else if ((o instanceof Book)) { return super.equals(o); } return false; } }
Реализация Ebook.equals рассматривает два случая:
- Сравниваемый объект имеет тип Ebook
- Мы сравниваем экземпляр Ebook с экземпляром Book. Для этого мы вызываем метод из суперкласса, чтобы сравнить ту часть, которую можно сравнить – только код ISBN.
Нетрудно заметить, что оба метода обеспечивают маневренность, симметрию и согласованность. Однако давайте посмотрим, как выглядит ситуация с переходным. Равенство, реализованное таким образом, не является транзитивным. Чтобы убедиться в этом, просто проанализируйте следующий случай:
Code language: JavaScript (javascript)
Book b1 = new Book("1"); Ebook e1 = new Ebook("1", "mobi"), e2 = new Ebook("1", "epub"); e1.equals(b1) -> true (1) b1.equals(e2) -> true (2) e1.equals(e2) -> false (3)
Как мы видим, операция (3) возвращает false, что противоречит ожиданиям от переходного оператора.
А как насчет этой проходимости?
Заметим, что из представленного примера мы можем сделать немедленный, более общий вывод. А именно, что невозможно реализовать метод равенства, т.е. относиться к электронной книге так, как если бы это была обычная книга. Это – позвольте мне сказать это еще раз – естественное следствие сравнения сущностей разных типов, как в реальном мире, так и в объектно-ориентированном смысле.
Как же можно решить эту проблему? В этом случае можно сделать две вещи:
- Запретить наследование классов, которые являются так называемыми value classes (классы, представляющие ценности или объекты реального мира). На самом деле это вполне разумно как с точки зрения моделирования реального мира, так и с технической точки зрения – такая процедура значительно упрощает реализацию equals.
- Если по каким-то причинам наш класс должен быть открыт для возможного наследования, можно считать, что объекты разных типов никогда не бывают одинаковыми. Тогда реализация также очень проста. Шаблон метода при таком подходе может выглядеть следующим образом:
public boolean equals(Object o) { if(o == null) return false; if(o.getClass() != this.getClass()) return false; ... // просто сравните поля }
Code language: JavaScript (javascript)
При таком подходе обратите внимание, что такой метод не ведет себя корректно для производных классов.
Давайте вернемся на мгновение к основной проблеме.
Неужели невозможно реализовать метод equals так, чтобы он отвечал всем требованиям и в то же время мог сравнивать объекты с разных уровней иерархии?
В целом, существуют приемы, позволяющие правильно реализовать их.
Однако вопрос остается открытым: действительно ли разумно, что объекты разных типов могут быть равны?
Во-вторых, такие решения обычно сложны и гораздо труднее реализуемы, чем можно было бы ожидать от равных.
Поэтому кажется наиболее логичным считать, что классы, несущие ценности, должны быть классами, закрытыми для расширения.
Резюме
Реализация метода equals кажется очень простой.
Однако особое внимание следует уделить его правильному применению, поскольку несоблюдение его требований может привести к ошибкам, которые не всегда будут заметны на первый взгляд.
Программист, предоставляющий реализацию равных, должен тщательно проанализировать, соблюдает ли его функция все требуемые ограничения.