equals() жабиста

Aug 10, 2017 19:02

Эта статья - результат обсуждения реализации equals() со своей женой. У неё вышел спор по поводу реализации данного метода с типичными java-программистами, поле битвы осталось за ней :)
Для начала, вспомним, почему оператор == в java неэффективен (не работает так, как некоторые ожидают). Первое, что надо отметить, в Java - всё является указателем (ссылкой на указатель). Даже несмотря на то, что с адресами непосредственно работы не происходит. Именно поэтому и работает сборщик мусора :), поэтому же и оператор сравнения прост до безобразия: он проверяет 2 указателя на соответствие. В терминах с++-подобного языка код оператора следующий:

copy to clipboardподсветка кода
  1. bool operator == (Object* p1, Object* p2){  
  2.     return p1 == p2;  
  3. }  


Уж сравнить два целых числа Java сумеет.

Поэтому, чтобы сравнить два различных объекта, надо переопределить... В случае Java, тот самый метод equals. Обратите внимание, в С++ оператор сравнения определяется для статических объектов, чего Java программисты напрямую сделать не могут. Поэтому, типичный пример реализации метода equals следующий:

copy to clipboardподсветка кода
  1. class Entity {  
  2.     public Entity(int field) {  
  3.         this.field = field;  
  4.     }  
  5.     int field;  
  6.   
  7.     @Override  
  8.     public boolean equals(Object obj) {  
  9.         if (this == obj) {  
  10.             return true;  
  11.         }  
  12.         if (obj instanceof Entity == false) {  
  13.             return false;  
  14.         }          
  15.         return ((Entity) obj).field == field;  
  16.     }  
  17. }  
Подобный код генерируют NetBeans и Eclipse. Есть еще проверка на нулевой входной параметр, кажется, но она является лишней.

КТО ВИНОВАТ?
Так вот, эта реализация является неправильной. Вспомним, что мы знаем об операторе сравнения: он является
  • рефлексивным (A == A);
  • коммутативным(A == B <=> B == A);
  • транзитивным (A == B, B == C => A == C).
Так вот, данная реализация не удовлетворяет требованию коммутативности операции. Рассмотрим следующий пример:

copy to clipboardподсветка кода
  1. class Entity {  
  2.     public Entity(int field) {  
  3.         this.field = field;  
  4.     }  
  5.     int field;  
  6.   
  7.     @Override  
  8.     public boolean equals(Object obj) {  
  9.         if (this == obj) {  
  10.             return true;  
  11.         }  
  12.         if (obj instanceof Entity == false) {  
  13.             return false;  
  14.         }          
  15.         return ((Entity) obj).field == field;  
  16.     }  
  17. }  
  18.   
  19. class ChildEntity extends Entity {  
  20.     public ChildEntity(int field) {  
  21.         super(field);  
  22.     }  
  23.   
  24.     @Override  
  25.     public boolean equals(Object obj) {  
  26.         if (this == obj) {  
  27.             return true;  
  28.         }  
  29.         if (obj instanceof ChildEntity == false) {  
  30.             return false;  
  31.         }          
  32.         return ((ChildEntity) obj).field == field;  
  33.     }  
  34. }  
И для проверки нашего кода в функции main пропишем следующее:

copy to clipboardподсветка кода
  1. Entity e = new Entity(12);  
  2. ChildEntity ce = new ChildEntity(12);  
  3.           
  4. System.out.println(e.equals(ce) == ce.equals(e));  
После запуска мы неожиданно получаем в консоли false, то есть A == B, но B != A!
С чем это связано? С простым фактом, что наследование означает сужение множества значений. То есть любой элемент из класса-потомка (подмножество) есть в базовом классе (множестве), обратное утверждение неверно. По простому - в дочернем классе есть все, что определено в базовом классе, однако не у всех объектов базового класса есть все свойства дочернего.

ЧТО ДЕЛАТЬ?
Чтобы не было проблем с наследованием, надо сравнивать типы объектов. Верным является следующий код:

copy to clipboardподсветка кода
  1. class Entity {  
  2.     public Entity(int field) {  
  3.         this.field = field;  
  4.     }  
  5.     int field;  
  6.   
  7.     @Override  
  8.     public boolean equals(Object obj) {  
  9.         if (this == obj) {  
  10.             return true;  
  11.         }  
  12.         if (this.getClass() != obj.getClass() || obj == null) {  
  13.             return false;  
  14.         }  
  15.         return ((Entity) obj).field == field;  
  16.     }  
  17. }  
  18.   
  19. class ChildEntity extends Entity {  
  20.     public ChildEntity(int field) {  
  21.         super(field);  
  22.     }  
  23.   
  24.     @Override  
  25.     public boolean equals(Object obj) {  
  26.         if (this == obj) {  
  27.             return true;  
  28.         }  
  29.         if (this.getClass() != obj.getClass() || obj == null) {  
  30.             return false;  
  31.         }  
  32.         return ((ChildEntity) obj).field == field;  
  33.     }  
  34. }  
Как видно, используется рефлексия для определения типов. Данная реализация будет работать всегда правильно, однако меня (как, кстати и Джошуа Кириевски) смущает использование рефлексии в данном случае.PARALLEL UNIVERSE
Я давно предлагал (ссылку, возможно, добавлю позже) реализовывать оператор сравнения через метод ToString() - в случае С++. И в случае Java данная идея является верной, с некоторым уточнением. Все знают, что сериализация - это здорово, это полезно. И, по идее, два одинаковых объекта должны иметь одинаковое XML/JSON представление. Представьте себе, что каждый класс имеет методы для сериализации! Но, пока этого нет (можно реализовать через рефлексию времени компиляции), приходится пользоваться существующим в каждом классе методом toString. Для того, чтобы всё хорошо работало, естественно, надо переопределять данный метод. equals будет реализовываться следующим образом:

copy to clipboardподсветка кода
  1. class Entity {  
  2.     int field;  
  3.     public Entity(int field) {  
  4.         this.field = field;  
  5.     }  
  6.     public String toJSON(){  
  7.         return "{classname:Entity, properties:[field:"+field+"]}";  
  8.     }      
  9.     @Override  
  10.     public String toString(){  
  11.         return this.toJSON();  
  12.     }  
  13.     @Override  
  14.     public boolean equals(Object obj) {  
  15.         return this == obj || (obj != null && this.toString().equals(obj.toString()));  
  16.     }  
  17. }  
  18.   
  19. class ChildEntity extends Entity {  
  20.     public ChildEntity(int field) {  
  21.         super(field);  
  22.     }      
  23.     @Override  
  24.     public String toJSON(){  
  25.         return "{classname:ChildEntity, properties:[field:"+field+"]}";  
  26.     }  
  27.     @Override  
  28.     public String toString(){  
  29.         return this.toJSON();  
  30.     }  
  31.     @Override  
  32.     public boolean equals(Object obj) {  
  33.         return this == obj || (obj != null && this.toString().equals(obj.toString()));  
  34.     }  
  35. }  
Я записал всё в один return, можно разбить на пару ветвлений.
Вот такая история о java и методе equals...
UPDATE: Как было отмечено, основная проблема сравнения через сериализацию это скорость. В этом случае необходимо JSON-представление объекта вычислять заранее, тогда сравнение объектов ускоряется (в ущерб скорости создания объектов), как и сама сериализация...

мысли, программирование, java, статьи писать, много букв

Previous post Next post
Up