czwartek, 24 lutego 2011

Metody equlas i hashCode w Javie.

Niestety, aby dojść do meritum problemu, muszę część rzeczy powielić po raz kolejny, żeby nie szukać tego po innych stronach. Dopiero kolejny post będzie dotyczył konkretnie problemu equals i hashCode w kontekście użycia z Hibernatem.

Czyli, na początek, standardowo - jak powinno się zaimplementować metodę equals i hashCode.

1. Sygnatura metody.

Niby wszyscy wiedzą, że metoda eguals powinna zawsze wyglądać tak:
public boolean equals(Object other)
ale warto wspomnieć dlaczego np. nie można zastosować (dla obiektu Entity) czegoś takiego:
public boolean equals(Entity entity)
problemy pojawiają się, np. przy użyciu kolekcji:
Entity entity = new Entity(1);
Entity entity2 = new Entity(2);
System.out.println(entity.equals(entity2)); //zwraca true

Set<Entity> encje = new HashSet<Entity>();
encje.add(entity);
System.out.println(encje.contains(entity2)); //zwraca false
ostatnia linijka zwraca false, ponieważ źle nadpisaliśmy metodę equals() z klasy Object, która jako parametr przyjmuje Object. Dlatego nasza metoda to jedynie przeładowanie metody equals, a nie jej nadpisanie.

Reasumując, prawidłowa metoda equals ma sygnaturę:
@Override
public boolean equals(Object other)
i koniec kropka!!!!! Strażnikiem poprawności sygnatury jest adnotacja oczywiście @Override, jeśli kompilator nie warczy, że jest źle użyta, to z naszą sygnaturą jest wszystko OK (o ile nie dziedziczymy z jakiejś innej klasy...).


2. Kontrakt dla metody equals(). 

Metoda musi spełniać następujące warunki:
  • zwrotność, czyli x.equals(x) == true
  • symetryczność, czyli x.equals(y) == true, wtedy i tylko wtedy gdy y.equals(x) == true
  • przechodniość, czyli jeśli x.equals(y)==true i y.equals(z)==true, wtedy x.equals(z)==true
  • konsekwentność, czyli wielokrotne powtórzenie x.equals(y) zwraca zawsze tą wartość dla niezmienionych obiektów x i y.
  • dla wartości null (np. x.equals(null)) metoda zawsze powinna zwracać false
Największą trudność w implementacji sprawia punkt 3., dlatego warto się mu przyjrzeć dokładniej. Rozważmy następujące klasy:
public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
    //...
}
public class ColorPoint extends Point {
    private Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    } 
//łamie symetryczność
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == color;
    }
}  

Na pierwszy rzut oka wszystko jest ok, aczkolwiek złamany jest punkt drugi kontraktu, tj. symetryczność:
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);

p.equals(cp); //true
cp.equals(p); //false
możemy oczywiście poprawić metodę tak aby symetryczność została zachowana:
    //łamie przechodniość 
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        // If o is a normal Point, do a color-blind comparison
        if (!(o instanceof ColorPoint))
            return o.equals(this);
        // o is a ColorPoint; do a full comparison
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == color;
    } 
niestety, w tym przypadku łamana jest przechodniość:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED); 
Point p2 = new Point(1, 2); 
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

p1.equals(p2) //true
p2.equals(p3) //true
p1.equals(p3) //false 

Niestety powyższy problem nie ma jednego dobrego rozwiązania. Taka jest natura obiektowo zorientowanego programowania. Mało tego nawet niektóre obiekty języka Java mają ten problem, np equals() z klasy Timestamp (która jest podklasą Date) łamie symetryczność.

Można to obejść np. zamiast dziedziczenia Point dodać prywatne pole typu Point do klasy ColorPoint, wtedy:
class ColorPoint {
    private Point point;
    private Color color;

    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}
3. Przykład jak zaimplementować equals().
@Override
public boolean equals(Object object) {
    
    if (this == object) return true; //załatwia punkt 1. kontraktu
    
    if ( !(object instanceof Entity) ) return false; //dodatkowo załatwia punkt 5 kontraktu
    
    Entity entity = (Entity) object;

    //dla porównania konkretnych warto posłużyć się już gotowym builderem z biblioteki commons
    return new EqualsBuilder().append(field1, entity.getField1())
            .append(field2, entity.getField2())
            .isEquals();
}

ważnym jest aby dla buildera wybrać tylko te pola, mają znaczenie przy porównaniu!

4. Jeśli nadpisujesz equals(), to zawsze nadpisuj hashCode()

ale o tym w następnym poście.

4 komentarze:

  1. Nie uzywaj operatora instanceof tylko getClass(). Twoja implementacja jest bledna

    OdpowiedzUsuń
  2. A czy jest ku temu jakiś powód? Uważam, że wszystko jest poprawnie.

    OdpowiedzUsuń
  3. Jeśli klasa B dziedziczy po A, to "B instanceof A" zwraca prawdę, nastomiast getClass zwróciłoby fałsz.

    OdpowiedzUsuń
  4. W zasadzie masz rację, aczkolwiek, jak zwykle, to zależy od kontekstu: http://www.artima.com/intv/bloch17.html

    OdpowiedzUsuń