Простое доказательство контракта equals & hashCode

Привет! Это дополнение статьи про equals и hashCode в java. В этом дополнении я бы хотел рассказать про доказательство контракта между этими методами без всяких hash структур (чтобы любому новичку было понятно) + я бы хотел привести весь контракт между методами в этом дополнении, в прошлой статье я написал только о том, что если

если equals дает true, то и hashCode должны выдавать одинаковые хеши

Но это только половина контракта, давайте опишем его целиком

Если equals дает true, то и hashCode должны быть одинаковы, но если хеш коды одинаковы, то equals не обязательно должен быть true

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

Давайте напишем простой класс и назовем его A и реализуем в нем equals и hashCode

 private static class A {
private final String name;
private final int age;

public A(String name, int age) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name must be not " +
"null and not be empty");
}

this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof A)) return false;

A o = (A) obj;

return name.equals(o.name)
&& age == o.age;
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}

Класс у нас тут private static потому что он вложен в другой класс (дальше будет приведен весь код). Это простой объект, принимает два параметра, имя и возраст и реализует два метод equals и hashCode. Почему в equals используется instanceof я объясню. Потому что использование getClass() == getClass() приводит к проблемам сравнения базового типа и дочернего, давайте простым языком.

Вот представьте, есть у нас класс A, в нем есть поля и методы equals + hashCode, и есть у нас класс B, он наследуется от А, но в A в equals такой код

public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || obj.getClass() != getClass()) return false;

// дальше приведение типов и сравнение значений, нам не важно
}

Как думаете, что будет, если мы попытаемся выполнить такой код?

A a = new A();
// Помним, что B наследуется от A
B b = new B();

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

На консоль выведется false и все потому что мы сравниваем классы, у B и у A будут разные классы, а ведь B можно привести к A, так как это наследник и сравнить поля из А, но это невозможно из-за проверки на классы, по-этому используйте instanceof вместо сравнения классов. Кстати и instanceof не заставляет делать проверки на null, так как такая конструкция (например)

null instanceof A

всегда будет возвращать false.

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

 private static class B {
private final String name;
private final int age;

public B(String name, int age) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name must be not " +
"null and not be empty");
}

this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof B)) return false;

B o = (B) obj;

return name.equals(o.name)
&& age == o.age;
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}

Здесь разница лишь в том, что в equals мы теперь проверяем тип B. Давайте теперь все это запустим

 final String name = "John";
final int age = 20;

A a = new A(name, age);
B b = new B(name, age);

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

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

Как думаете что выведет код?

false
true
false
true

Контракт доказан, equals возвращает false, так как объекты разных типов (проверка на instanceof), но hashCode возвращает одинаковые значения, потому что значения полей в классе одинаковы и hash для них соответственно одинаковый, пример можно упростить донельзя, но я старался сохранить более менее реальность примера, можно сделать пример еще сложнее и сделать вообще разные классы, но чтобы они возвращали одинаковый хеш, но это будет сложно понимать новичкам

Весь код

import java.util.Objects;

public class EqualsAndHashCodeContract {
public static void main(String[] args) {
final String name = "John";
final int age = 20;

A a = new A(name, age);
B b = new B(name, age);

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

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

private static class A {
private final String name;
private final int age;

A(String name, int age) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name must be not " +
"null and not be empty");
}

this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof A)) return false;

A o = (A) obj;

return name.equals(o.name)
&& age == o.age;
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}

private static class B {
private final String name;
private final int age;

B(String name, int age) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name must be not " +
"null and not be empty");
}

this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof B)) return false;

B o = (B) obj;

return name.equals(o.name)
&& age == o.age;
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
}

Заключение

Контракт доказан, пишите правильно свои методы equals & hashCode

 

Java
22.04.2018
1 ответ
авторизуйтесь чтобы ответить