вторник, 31 января 2012 г.

Equals, hashCode


Метод equals() обозначает отношение эквивалентности объектов. Эквивалентным называется отношение, которое является симметричным, транзитивным, рефлексивным и постоянным.
  • Рефлексивность: для любого ненулевого x, x.equals(x) вернет true;
  • Транзитивность: для любого ненулевого x, y и z, если x.equals(y) и y.eqals(z) вернет true, тогда и x.equals(z) вернет true;
  • Постоянство: для любых объектов x и y x.equals(y) возвращает одно и тоже, если информация, используемая в сравнениях, не меняется;
  • Симметричность: для любого ненулевого x и y, x.equals(y) должно вернуть true, тогда и только тогда, когда y.equals(x) вернет true.


При сравнении своей реализации объектов необходимо переопределять метод equals. Так же при переопределении equals, необходимо производить переопределить метод hashCode(), но об этом поговорим позже.
Предположим, что у нас есть класс App, в котором есть три поля: str1, str2 и num. Нам необходимо провести сравнение экземпляров объектов на равенство, его полей.
Сначала напишем класс без переопределения.

package my.Equals;

public class App
{
       String str1 = new String("Test1");
       String str2 = new String("Test2");
       int num = 5;

       public static void main( String[] args )
    {
             App app1 = new App();
App app2 = new App();
            
System.out.println( app1.equals(app2) );
    }
}
/* результат:
flase
*/

Как вы видите, на выходе получаем false. Это кажется странным, ведь оба экземпляра класса равны. Почему же получился такой результат? Все очень просто. В базовом классе Object метод equals сравнивает содержимое объектов. В нашем случае сравниваются ссылки на класс, а они как не трудно догадаться, не равны. Что бы избежать данной ошибки необходимо произвести переопределение метода equals.

package my.Equals;

public class App
{
       String str1 = new String("Test1");
       String str2 = new String("Test2");
       int num = 5;

       @Override
       public boolean equals(Object obj) {
             // проверяет не равен ли obj null
             if(obj == null) {return false;}

// проверяет является ли obj объектом App
             if(!(obj instanceof App)){return false;}
            
             App obj1 = (App) obj;
            
             // сравнивает поля экземпляров класса
             return str1.equals(obj1.str1)
&& str2.equals(obj1.str2)
&& num == obj1.num;
       }
      
       public static void main( String[] args )
    {
             App app1 = new App();
App app2 = new App();
            
System.out.println( app1.equals(app2) );
    }
}
/* результат:
true
*/

Теперь мы получили то, что и ожидали.
Если чуть изменить наш пример, и прописать в методе main:

App app1 = new App();
App app2 = new App();
App2.str1 = "New test2"; 
System.out.println( app1.equals(app2) );

то метод equals вернет false.
Еще одной важной деталью является то, что тип аргумента, метода equals, должен быть Object, а не классом, который сравнивается.

Как я и обещал, поговорим о методе hashCode().
При переопределении метода equals() необходимо переопределять метод hashCode(). Это связано с тем, что для идентичных объектов разных экземпляров класса  могут возвращаться разные значения. А это не всегда является корректным.
Пример переопределения метода hashCode. Для этого немного изменим предыдущий пример.

package my.Equals;

public class App
{
       String str1 = new String("Test1");
       String str2 = new String("Test2");
       int num = 5;

       @Override
       public boolean equals(Object obj) {
            
             if(obj == null) {return false;}

             if(!(obj instanceof App)){return false;}
            
             App obj1 = (App) obj;
            
             return str1.equals(obj1.str1)
&& str2.equals(obj1.str2)
&& num == obj1.num;
       }
      
       @Override
       public int hashCode() {
             int hash = 37;
             hash = hash*17 + str1.hashCode();
             hash = hash*17 + str2.hashCode();
             hash = hash*17 + num;
                          
             return hash;
       }

       public static void main( String[] args )
    {
             App app1 = new App();
             App app2 = new App();
            
             System.out.println("HashCode: app1.str1 - "
+ app2.str1.hashCode() + "; app2.str1 - "
+ app2.str1.hashCode());
             System.out.println("HashCode: app1.str2 - "
+ app2.str2.hashCode() + "; app2.str - "
+ app2.str2.hashCode());

System.out.println( app1.equals(app2) );
       
             // изменяем значение str1, для экземпляра класса app2
             app2.str1 = "test";
System.out.println("HashCode: app1.str1 - "
+ app2.str1.hashCode() + "; app2.str1 - "
+ app2.str1.hashCode());
             System.out.println("HashCode: app1.str2 - "
+ app2.str2.hashCode() + "; app2.str - "
+ app2.str2.hashCode());
System.out.println( app1.equals(app2) );

    }
}
/* результат:
* HashCode: app1.str1 - 80698815; app2.str1 - 80698815
* HashCode: app1.str2 - 80698816; app2.str - 80698816
* true
*
* HashCode: app1.str1 - 3556498; app2.str1 - 3556498
* HashCode: app1.str2 - 80698816; app2.str - 80698816
* false
*/

Как видно из примера, при правильном переопределении метода, у идентичных объектов экземпляра класса hash code имеет одинаковое значение.
Однако одинаковое значение объектов не всегда гарантирует эквивалентность объектов. Это можно увидеть по второй части результата примера.
Отсюда вытекает три правила:
·         При переопределении метода equals необходимо переопределять метод hashCode;
·         Если объекты экземпляров класса эквивалентны, согласно equals,  то и hashCode  должен возвращать одинаковые значения;
·         Однако при одинаковых значениях hashCode, объекты могут быть не эквивалентными.

3 комментария:

  1. интересно было бы, если при
    System.out.println("HashCode: app1.str1 - "
    + app2.str1.hashCode() + "; app2.str1 - "
    + app2.str1.hashCode());

    результат был бы разным, пытаясь вывести одно и то же

    ОтветитьУдалить
  2. В примере опечатка
    "HashCode: app1.str1 - "+ app2.str1.hashCode()

    нужно заменить на:
    "HashCode: app1.str1 - "+ app1.str1.hashCode()

    ОтветитьУдалить