equals(Object obj)
w Javie, to wiadomo jedno - wcale nie jest taka trywialna do napisania, zwłaszcza że jednocześnie trzeba pamiętać o hashCode()
. Na szczęście nowoczesne IDE takie jak Eclipse czy NetBeans potrafią same wygenerować poprawną parę tych metod. Ale czy aby na pewno?Przypomnijmy sobie czego Sun (ups, Oracle) wymaga jako "kontraktu" metody
equals
(http://download.oracle.com/javase/1.4.2/docs/api/java/lang/Object.html#equals%28java.lang.Object%29):1. Powinna to być relacja równoważności, czyli musi być:
a) zwrotna:
x
musi być równe x
,b) symetryczna: jeżeli
x=y
, to y=x
,c) przechodnia: jeżeli
x=y
i y=z
, to x=z
2. Musi być spójna, tzn. ta sama para
x
i y
musi zawsze dawać ten sam wynik.3. I musi też zachodzić:
x != null
Przesłaniając metodę
equals
trzeba pamiętać o wszystkich pięciu warunkach - na szczęście naszej pamięci przychodzą na pomoc nowoczesne IDE :)I tak np. Eclipse dla klasy:
public class Number {
private int x;
}
wygeneruje następującą metodę
equals
:@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Number other = (Number) obj;
if (x != other.x)
return false;
return true;
}
I wszystko na razie gra, mamy piękną klasę Number, którą Eclipse nam wygenerował prawie w całości. Teraz w naszym Programie napisaliśmy prostą metodę, która sprawdzi czy dana liczba jest małą liczbą pierwszą:
public class Program {
private final static Set SMALL_PRIMES = new HashSet();
static {
SMALL_PRIMES.add(new Number(2));
SMALL_PRIMES.add(new Number(3));
SMALL_PRIMES.add(new Number(5));
SMALL_PRIMES.add(new Number(7));
}
private static boolean isSmallPrime(Number number)
{
return SMALL_PRIMES.contains(number);
}
public static void main(String[] args)
{
Number number2 = new Number(2);
System.out.println(isSmallPrime(number2));
}
}
Nie pytajcie o sens tych programów, to tylko przykłady ;)
Dobra, wszystko świetnie, ale zmiana występuje zawsze i od dzisiaj nasze liczby będą mogły mieć również kolory ;) Rozszerzamy więc prostą klasę poprzez dziedziczenie:
import java.awt.Color;
public class ColorNumber extends Number {
private Color color;
public ColorNumber(int x, Color color) {
super(x);
this.color = color;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((color == null) ? 0 : color.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
ColorNumber other = (ColorNumber) obj;
if (color == null) {
if (other.color != null)
return false;
} else if (!color.equals(other.color))
return false;
return true;
}
}
Znowu metodę
equals
i prawie całą resztę wygenerował nasz kolega Eclipse. Pełni szczęścia dopisujemy do metody main
:Number blue2 = new ColorNumber(2, Color.BLUE);
System.out.println(isSmallPrime(blue2));
a naszym oczom ukazuje się
false
Eeee, ale co się stało? Najpierw trochę teorii...
Omawiając "problem kwadratu i prostokąta" (http://wojtek-m.blogspot.com/2010/07/problem-kwadratu-i-prostokata.html) przytoczyłem zasadę LSP (Liskov Substitution Principle, Zasadę Podstawienia Liskov). W praktyce (a nie teorii o literkach g, x, y, S i T) brzmi ona tak:
Funkcje instancji klas bazowych, muszą być w stanie używać również instancji klas dziedziczących po klasach bazowych, bez dokładnej znajomości dokładnego typu tych obiektów.
Czyli jak napiszemy kod, to powinno dać się go wykorzystać ponownie, nawet ze specjalizacjami klas będących argumentami. Oczywiście jeżeli te specjalizacje będą "sensownie" napisane - jeżeli ktoś mnożenie zaimplementuje w metodzie
add()
, no to sorry ;)Więc nasza metoda
isSmallPrime
albo jest źle napisana albo coś z klasą ColorNumber
jest nie tak. Hm... Na co stawiacie?Zgadliście, problem tkwi w metodzie
equals
(wykorzystywanej do znalezienie elementu zbioru), a dokładniej w tej linijce klasy Number
:if (getClass() != obj.getClass()) return false;
No tak, przecież
ColorNumber
to nie ta sama klasa co Number
. A więc pełni nadziei zamieniamy te linijki na operator instanceof
, tak żeby liczby porównywać z kolorowymi krewniakami nie patrząc na kolory:W Number (edit: jak Herbi dobrze zauważył, trzeba uważać na przepełnienie stosu):
if (!(obj instanceof Number)) return false;
I w ColorNumber:
if (!(obj instanceof ColorNumber)) return obj.equals(this); // w ColorNumber
Jednak teraz mamy znacznie większy problem. Nasza metoda
equals
nie jest już przechodnia:Number red3 = new ColorNumber(3, Color.RED);
Number number3 = new Number(3);
Number blue3 = new ColorNumber(3, Color.BLUE);
System.out.println(red3.equals(number3)); // true
System.out.println(number3.equals(blue3)); // true
System.out.println(red3.equals(blue3)); // ups! false
Hm... Niestety z tego zaklętego kręgu nie ma wyjścia - kiedy chcemy przeciążyć
equals
w klasie, która dziedziczy po innej klasie konkretnej to * albo nasz operator równości nie będzie przechodni
* albo istnieje niebezpieczeństwo złamania zasady LSP...
Łamanie zasady LSP to mniejsze zło, z tego też powodu Eclipse jak i NetBeans generują metodę
equals
z użyciem getClass()
do dokładnego porównania typu parametru.Szkoda tylko że Eclipse nie napisze żadnego komentarza z ostrzeżeniem, bo konia z rzędem temu, kto trafi na podobny błąd w prawdziwej aplikacji i potrafi znaleźć przyczynę...
Aha, ten post to nie pojedynczy wybryk :) Po blisko 4 miesięcznej przerwie powracamy ;) Jednak nie do systemu "nowy post co 2 dni", ale "nowy post raz na jakiś czas, może nawet raz na tydzień"
no fakt, nie zrobisz metody, która w jednym przypadku będzie pomijała jedno z pól (pomijała, bo w klasie bazowej go nie ma), a w drugim przypadku będzie z niego korzystała, bo to będzie niezgodne z kontraktem, ale na coś trzeba się zdecydować, albo brać zawsze do porównania kolor (a jeśli go nie ma, zwracać false), albo nigdy go nie brać pod uwagę (i móc porównywać potomka z rodzicem).
OdpowiedzUsuńCo do:
if (!(obj instanceof Number)) return obj.equals(this)
to wszystko fajnie (poza tym, że - jak słusznie zauważyłeś - takie coś nie spełnia kontraktu), ale jak ktoś pójdzie twoim śladem i zrobi sobie klasę A, która będzie miała w metodzie equals:
if (!(obj instanceof A)) return obj.equals(this)
to wywołanie new Number.equals(new A) skończy się przepełnieniem stosu :)
Tak, racja, w Number
OdpowiedzUsuńreturn obj.equals(this);
nie ma sensu, powinno być
return false;
Dzięki za zwrócenie uwagi ;) Co nie zmienia faktu, że i tak nie należy tak robić ;)