Эта статья - результат обсуждения реализации equals() со своей женой. У неё вышел спор по поводу реализации данного метода с типичными java-программистами, поле битвы осталось за ней :)
Для начала, вспомним, почему оператор == в java неэффективен (не работает так, как некоторые ожидают). Первое, что надо отметить, в Java - всё является указателем (ссылкой на указатель). Даже несмотря на то, что с адресами непосредственно работы не происходит. Именно поэтому и работает сборщик мусора :), поэтому же и оператор сравнения прост до безобразия: он проверяет 2 указателя на соответствие. В терминах с++-подобного языка код оператора следующий:
copy to clipboard
подсветка кода- bool operator == (Object* p1, Object* p2){
- return p1 == p2;
- }
Уж сравнить два целых числа Java сумеет.
Поэтому, чтобы сравнить два различных объекта, надо переопределить... В случае Java, тот самый метод equals. Обратите внимание, в С++ оператор сравнения определяется для статических объектов, чего Java программисты напрямую сделать не могут. Поэтому, типичный пример реализации метода equals следующий:
copy to clipboard
подсветка кода- class Entity {
- public Entity(int field) {
- this.field = field;
- }
- int field;
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj instanceof Entity == false) {
- return false;
- }
- return ((Entity) obj).field == field;
- }
- }
Подобный код генерируют NetBeans и Eclipse. Есть еще проверка на нулевой входной параметр, кажется, но она является лишней.
КТО ВИНОВАТ?
Так вот, эта реализация является неправильной. Вспомним, что мы знаем об операторе сравнения: он является
- рефлексивным (A == A);
- коммутативным(A == B <=> B == A);
- транзитивным (A == B, B == C => A == C).
Так вот, данная реализация не удовлетворяет требованию коммутативности операции. Рассмотрим следующий пример:
copy to clipboard
подсветка кода- class Entity {
- public Entity(int field) {
- this.field = field;
- }
- int field;
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj instanceof Entity == false) {
- return false;
- }
- return ((Entity) obj).field == field;
- }
- }
-
- class ChildEntity extends Entity {
- public ChildEntity(int field) {
- super(field);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj instanceof ChildEntity == false) {
- return false;
- }
- return ((ChildEntity) obj).field == field;
- }
- }
И для проверки нашего кода в функции main пропишем следующее:
copy to clipboard
подсветка кода- Entity e = new Entity(12);
- ChildEntity ce = new ChildEntity(12);
-
- System.out.println(e.equals(ce) == ce.equals(e));
После запуска мы неожиданно получаем в консоли false, то есть A == B, но B != A!
С чем это связано? С простым фактом, что наследование означает сужение множества значений. То есть любой элемент из класса-потомка (подмножество) есть в базовом классе (множестве), обратное утверждение неверно. По простому - в дочернем классе есть все, что определено в базовом классе, однако не у всех объектов базового класса есть все свойства дочернего.
ЧТО ДЕЛАТЬ?
Чтобы не было проблем с наследованием, надо сравнивать типы объектов. Верным является следующий код:
copy to clipboard
подсветка кода- class Entity {
- public Entity(int field) {
- this.field = field;
- }
- int field;
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (this.getClass() != obj.getClass() || obj == null) {
- return false;
- }
- return ((Entity) obj).field == field;
- }
- }
-
- class ChildEntity extends Entity {
- public ChildEntity(int field) {
- super(field);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (this.getClass() != obj.getClass() || obj == null) {
- return false;
- }
- return ((ChildEntity) obj).field == field;
- }
- }
Как видно, используется рефлексия для определения типов. Данная реализация будет работать всегда правильно, однако меня (как, кстати и Джошуа Кириевски) смущает использование рефлексии в данном случае.PARALLEL UNIVERSE
Я давно предлагал (ссылку, возможно, добавлю позже) реализовывать оператор сравнения через метод ToString() - в случае С++. И в случае Java данная идея является верной, с некоторым уточнением. Все знают, что сериализация - это здорово, это полезно. И, по идее, два одинаковых объекта должны иметь одинаковое XML/JSON представление. Представьте себе, что каждый класс имеет методы для сериализации! Но, пока этого нет (можно реализовать через рефлексию времени компиляции), приходится пользоваться существующим в каждом классе методом toString. Для того, чтобы всё хорошо работало, естественно, надо переопределять данный метод. equals будет реализовываться следующим образом:
copy to clipboard
подсветка кода- class Entity {
- int field;
- public Entity(int field) {
- this.field = field;
- }
- public String toJSON(){
- return "{classname:Entity, properties:[field:"+field+"]}";
- }
- @Override
- public String toString(){
- return this.toJSON();
- }
- @Override
- public boolean equals(Object obj) {
- return this == obj || (obj != null && this.toString().equals(obj.toString()));
- }
- }
-
- class ChildEntity extends Entity {
- public ChildEntity(int field) {
- super(field);
- }
- @Override
- public String toJSON(){
- return "{classname:ChildEntity, properties:[field:"+field+"]}";
- }
- @Override
- public String toString(){
- return this.toJSON();
- }
- @Override
- public boolean equals(Object obj) {
- return this == obj || (obj != null && this.toString().equals(obj.toString()));
- }
- }
Я записал всё в один return, можно разбить на пару ветвлений.
Вот такая история о java и методе equals...
UPDATE: Как было отмечено, основная проблема сравнения через сериализацию это скорость. В этом случае необходимо JSON-представление объекта вычислять заранее, тогда сравнение объектов ускоряется (в ущерб скорости создания объектов), как и сама сериализация...