Что лучше использовать для сравнения типов классов у переменных при написании equals? Я уже описывал этот метод в специальной статье. С одной стороны, я часто видел использование instanceof:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class MyObject { private String field; public String getField() { return field; } public void setField(String field) { this.field = field; } @Override public boolean equals(final Object other) { if (!(other instanceof MyObject)) { return false; } MyObject castOther = (MyObject) other; return Objects.equals(field, castOther.field); } @Override public int hashCode() { return Objects.hash(field); } } |
На первый взгляд, здесь всё впорядке. Но давайте создадим дочерний класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class MyChildObject extends MyObject { private boolean v1; public boolean isV1() { return v1; } public void setV1(boolean v1) { this.v1 = v1; } @Override public boolean equals(final Object other) { if (!(other instanceof MyChildObject)) { return false; } MyChildObject castOther = (MyChildObject) other; return Objects.equals(v1, castOther.v1); } @Override public int hashCode() { return Objects.hash(v1); } } |
Вроде тоже ничего плохого. Но давайте проведём следующий эксперимент:
1 2 3 4 5 6 7 8 9 |
MyChildObject child = new MyChildObject(); child.setField("str1"); child.setV1(true); MyObject my = new MyObject(); my.setField("str1"); System.out.println("my.equals(child) = " + my.equals(child)); System.out.println("child.equals(my) = " + child.equals(my)); |
Результат будет такой:
1 2 |
my.equals(child) = true child.equals(my) = false |
Но в договорённости о работе equals в официальном JavaDoc сказано, что метод должен быть симметричным. Симметричность означает, что для каждой пары x и y у которой x.equals(y) должно быть равно y.equals(x). Значит, наши методы некорректны. Перепишем их на getClass:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class MyObject { ... @Override public boolean equals(final Object other) { if (other == null) { return false; } if (!getClass().equals(other.getClass())) { return false; } MyObject castOther = (MyObject) other; return Objects.equals(field, castOther.field); } ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class MyChildObject { ... @Override public boolean equals(final Object other) { if (other == null) { return false; } if (!getClass().equals(other.getClass())) { return false; } MyChildObject castOther = (MyChildObject) other; return Objects.equals(v1, castOther.v1); } @Override public int hashCode() { return Objects.hash(v1); } ... } |
Проведём эксперимент ещё раз:
1 2 |
my.equals(child) = false child.equals(my) = false |
Теперь класс может быть равен другому классу только если он строго того же типа, что и второй класс. Зато выполняется правило рефлексии для equals.
Вывод: Для equals всегда нужно сравнивать типы объектов через getClass.
Все методы equals и hashCode для данной статьи были сгенерированы с помощью плагина Jenerate для Eclipse.
equals не всегда симметричен.
в той же HashMap это не так.
А что, если наш объект — энтити хибернейт? getClass у такого объекта укажет нам на прокси и мы не сможем добраться до сравнения полей объекта.
Ну да, возможно, для Hibernate это не всегда подходит, хотя и тоже спорно. Но требование по симметричности вообще было в самой документации от Oracle, поэтому я его здесь привёл.