Как написать equals() в Java
К концу этой статьи будет предоставлена вся необходимая информация, чтобы можно было написать лучший метод equals() на языке программирования Java. Детали равенства будут рассмотрены с точки зрения математического определения и в манере языка программирования Java, чтобы охватить фундаментальную информацию, необходимую для использования наилучшей практики метода equals( ) .
Математическое определение равенства
Согласно Википедии , определение математического равенства таково:
В математике равенство — это отношение между двумя величинами или, в более общем смысле, двумя математическими выражениями, утверждающее, что величины имеют одинаковое значение или что выражения представляют один и тот же математический объект. Равенство между A и B записывается как A = B , и произносится как A равно B .
Определение равенства в Java
В языке программирования Java равенство означает просто наличие одного и того же значения. Иногда буквально подразумевается, что он имеет то же значение, а иногда подразумевается, что он имеет одинаковую структуру с точки зрения свойств объекта.
Когда необходимо проверить равенство двух значений примитивного типа, непосредственно используется знак равенства, как в примере ниже;
int a = 5;
int b = 6;
int c = 5;
// Prints "false" since a and b do not have the same value.
System.out.println(a == b);
// Prints "true" since a and c have the same value.
System.out.println(a == c);
С другой стороны, иногда необходимо проверить равенство двух объектов. С точки зрения объектов существует два разных подхода, которые используются на разных этапах процесса проверки равенства двух объектов. Давайте углубимся в детали.
Ссылочное равенство
В языке программирования Java фактические значения объектов хранятся где-то в памяти, и на них указывают ссылки на объекты, которые также хранятся где-то в памяти. Давайте посмотрим на следующий фрагмент кода;
// Create an object with "new" operator.
// Point to object with object reference "a".
Object a = new Object();
// Create another object reference.
// Point to the same object as the reference "a" is pointing.
Object b = a;
// Output is true.
System.out.println(a == b);
// Create another object with "new" operator.
// Point to object with object reference "b".
b = new Object();
// Output is false.
System.out.println(a == b);
В таком сценарии «a» и «b» на самом деле являются двумя ссылками, указывающими на один и тот же объект, поэтому можно прямо сказать, что «a» равно «b», поскольку достигается ссылочное равенство.
Однако обратный сценарий действует не всегда. Две разные ссылки могут указывать на разные ячейки памяти, но объекты, на которые указывают ссылки, могут быть одинаковыми.
В обычной жизни иногда сравнивают величины. Например, если у вас есть два яблока и у вашего друга два яблока, то количество яблок у вас и вашего друга одинаково.
С другой стороны, иногда вещи также можно сравнивать с точки зрения их свойств или структуры. Давайте углубимся в детали этого сценария.
Структурное равенство
Структурное равенство — это тип, при котором объекты сравниваются по состоянию их свойств. Для проверки этого типа равенства необходим более сложный инструмент, а не только оператор «==». Это метод equals(). Он происходит от класса Object, который находится в высшей точке иерархии классов в языке программирования Java. Давайте проверим предстоящий фрагмент кода Java;
public class Car {
// Assume that we have corresponding constructor, accessors, mutators, etc.
private String brand;
private String model;
private Integer modelYear;
private Integer mileage;
}
Car car1 = new Car("Audi","R8", 2023);
Car car2 = new Car("Audi","R8", 2023);
Car car3 = new Car("Audi","R8", 2021);
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));
public boolean equals(Object obj) {
return (this == obj);
}
public class Car {
private String brand;
private String model;
private Integer modelYear;
private Integer mileage;
// Assume that we have corresponding constructor, accessors, mutators, etc.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
}
}
Во второй строке начинаем проверять подлинность объектов. Если ссылка на объект о имеет значение null или обе ссылки на объекты указывают на объекты, принадлежащие разным классам, то можно прямо сказать, что эти два объекта не имеют одинаковой идентичности и не равны. Если это не так, то мы, наконец, можем проверить фактическую структуру объектов.
В третьей строке метода мы приводим ссылку o к соответствующему типу класса, поскольку метод переопределен из класса Object, и теперь мы знаем, что он принадлежит классу Car.
Наконец, в последней строке метода проверяется структура двух объектов, и это фактическая строка, в которой определяется идентичность. Проще говоря, если марки, модели и годы выпуска одинаковы, то эти два объекта равны. Другими словами, это структурное равенство.
Мы изучали свойства математического равенства в начальной школе. Некоторые из этих свойств также должны быть допустимы для метода equals() . Это означает, что эти свойства должны быть добавлены к ингредиентам метода equals() . Давайте посмотрим на эти свойства с точки зрения математического равенства и с точки зрения равенства в Java;
Рефлексивное свойство
В математическом определении рефлексивного свойства для всех действительных чисел x ;
х = х
Число равно самому себе.
С точки зрения равенства Java объект должен быть равен самому себе;
Car car1 = new Car("Audi","R8",2013);
System.out.println(car1.equals(car1));
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return mileage < car.mileage && brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
}
Симметричное свойство
С точки зрения математического определения симметричного свойства для всех действительных чисел x и y ;
Если х = у , то у = х
Порядок равенства не имеет значения.
С точки зрения равенства Java, если объект a равен объекту b, то объект b должен быть равен объекту a;
Car car1 = new Car("Audi","R8",2013);
Car car2 = new Car("Audi","R8",2013);
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car1));
public class Car {
private String brand;
private String model;
private Integer modelYear;
private Integer mileage;
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Car)) return false;
Car car = (Car) o;
return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
}
}
public class SportCar extends Car {
private String engineType;
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof SportCar)) return false;
SportCar sportCar = (SportCar) o;
return engineType.equals(sportCar.engineType);
}
}
Car car1 = new Car("Audi", "R8", 2012, 1000);
SportCar car2 = new SportCar("Audi", "R8", 2012, 1000, "4.0");
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car1));
Переходное свойство
В математическом определении транзитивности для всех действительных чисел x , y и z ;
если x = y и y = z , то x = z
Два числа, равные одному и тому же числу, равны между собой.
С точки зрения равенства Java, если объект a равен объекту b, а объект b равен объекту c, то объект a должен быть равен объекту c;
SportCar car1 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("100RPM"));
Car car2 = new Car("Audi", "R8", 2012, 1000);
SportCar car3 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("101RPM"));
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));
System.out.println(car1.equals(car3));
public class SportCar extends Car {
private Turbo turbo;
@Override
public boolean equals(Object o) {
if (!(o instanceof Car)) return false;
if (!(o instanceof SportCar)) return o.equals(this);
return super.equals(o) && ((SportCar) o).turbo == turbo;
}
}
public class Turbo {
private String power;
public Turbo(String power) {
this.power = power;
}
}
SportCar car1 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("100RPM"));
Car car2 = new Car("Audi", "R8", 2012, 1000);
SportCar car3 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("101RPM"));
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));
System.out.println(car1.equals(car3));
Согласованное свойство
Это другое свойство, поскольку математически точного эквивалента не существует. Для этого нам нужно получить тот же результат при последовательном выполнении метода equals().
Давайте посмотрим на фрагмент кода ниже;
Car car1 = new Car("Audi", "R8", 2012, 1000);
Car car2 = new Car("Audi", "R8", 2012, 1000);
for (int i = 0; i < 100000; i++) {
System.out.println(car1.equals(car2));
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Car)) return false;
Car car = (Car) o;
Random random = new Random();
return brand.equals(car.brand) && model.equals(car.model) && (random.nextInt(10) + 2013) == car.modelYear;
}
Примечание. Этот пример меня не очень удовлетворяет. После обширных исследований я заменю его на лучший. В документации Java к методу equals() четко сказано, что «множественные вызовы x.equals(y)последовательно возвращаются trueили последовательно возвращаются false». Для этого утверждения должен быть веский пример нарушения.
Свойство нулевого параметра
Вот как я решил назвать это свойство.
Пожалуйста, посмотрите на приведенный ниже фрагмент кода;
Car car1 = new Car("Audi", "R8", 2012, 1000);
Car car2 = null;
System.out.println(car1.equals(car2));
Теперь понятно, какими свойствами должна обладать реализация метода equals(), чтобы обеспечить корректное вычисление равенства. Кроме того, мы увидели возможные сценарии нарушения, поэтому теперь мы можем их избежать ради качества кода и бизнес-логики.
Сегодня многие IDE предоставляют механизмы генерации для создания наилучших возможных реализаций метода equals() . Рекомендуется придерживаться их большую часть времени вместо реализации метода equals() . Давайте посмотрим на пример из Intellij IDEA ;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear) && mileage.equals(car.mileage);
}
В первой строке проверяется ссылочное равенство, так как две ссылки на объект могут указывать на один и тот же объект.
Во второй строке обеспечивается проверка свойства нулевого параметра , а также то же самое, если условие действует как предохранительное предложение if, контролируя тип класса объектов, на которые указывают ссылки, чтобы предотвратить нарушение транзитивного свойства и симметричного свойства . .
В третьей строке ссылка на объект приводится к соответствующему типу класса, которым в нашем примере является Car, чтобы получить доступ к свойствам объекта Car.
Наконец, в четвертой строке проверяется равенство с точки зрения структурного равенства.
Существуют и другие шаблоны для генерации метода equals() . Вы также можете пойти и проверить их.
Если вы хотите написать реализацию самостоятельно, имейте в виду, что реализация должна обеспечивать допустимость всех свойств метода equals() .
Когда мы подошли к концу статьи, остается еще очень важный пробел, который необходимо заполнить. Откройте IDE и попробуйте сгенерировать метод equals() . Вы увидите, что нет возможности генерировать только метод equals() без метода hashCode() . Из-за некоторых типов коллекций в языке программирования Java рекомендуется не отделять эти два метода друг от друга. В официальной документации Java также говорится, что обычно необходимо переопределять метод hashCode() всякий раз, когда переопределяется метод equals() . Я хотел бы дать соответствующую информацию о хешировании и методе hashCode() в другой своей статье.
Кроме того, иногда нам может потребоваться не придерживаться свойств метода equals() для некоторых исключительных случаев, таких как классы сущностей JPA. Он также заслуживает отдельной статьи.
В заключение
Все важные моменты для реализации правильного метода equals() описаны в этой статье. Я также рекомендую вам написать соответствующие модульные тесты для проверки проверки свойств метода equals() . Не забудьте посмотреть и другие ресурсы, которые можно найти в сети.
Во-первых, это кажется очень простой и примитивной темой, поскольку она основана на основах объектно-ориентированного проектирования. Однако, как видно из деталей, очень важно не пропустить эти основные понятия и полностью разобраться.

![В любом случае, что такое связанный список? [Часть 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































