Żeby zobrazować jaki co ma wpływ zrobimy tak - weżmiemy HashSet i będziemy wrzucać do niego 100.000 (100k) różnych elementów T2. Ale żeby było zabawniej każdy po 10 razy.
Ponieważ to HashSet (czyli Set) to na koniec powinniśmy mieć 100000 elementów (bo powtorki wypadną). I jeszcze zobaczmy ile to trwa.
Pomiary czasu sa bardzo nieprofesjonalne i robione na VMce, gdzie w tle działa dużo aplikacji (nie chce mi się wyłacząć) - więc tylko pi razy oko oddają co wychodzi... ale coś będzie widać.
Na początek wersja bez equals i hashCode
Kopiuj
public class T2 {
public final String zmienna3;
public final String zmienna4;
public T2(String zmienna3, String zmienna4) {
this.zmienna3 = zmienna3;
this.zmienna4 = zmienna4;
}
public static void main(String[] args) {
putAndCount(10_000);//to rozgrzewka
long startTime = System.currentTimeMillis();
System.out.println("Elementów=" + putAndCount(100_000));
final long endTime = System.currentTimeMillis();
System.out.println("czas="+ (endTime-startTime));
}
public static int putAndCount(int size) {
final HashSet<T2> mySet = new HashSet<>();
for ( int j = 0; j < 10; j++) {
for ( int i = 0; i < size; i++) {
final T2 value = new T2("x="+i, "y"+i);
mySet.add(value);
}
}
return mySet.size();
}
}
Wynik:
Kopiuj
Elementów=1000000
czas=1509
Od razu widać kiszkę, bo elementów wypadło nam milion, a powinno 100 000. No ale skad ten biedny HashSet mial wiedzieć, że wrzucamy powtórki, jak nie było equals()?
Dorzucamy equals:
Kopiuj
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
T2 t2 = (T2) o;
return Objects.equals(zmienna3, t2.zmienna3) &&
Objects.equals(zmienna4, t2.zmienna4);
}
(to z intellij wygenerowane. ale brzydactwo).
Wynik:
Kopiuj
Elementów=1000000
czas=1346
Czyli czas nieco krótszy (dziwne), ale elementów nadal milion. Sam equals nie pomógł.
Dodajmy zatem hashCode(). Wygenerowany róznież z automatu.
Kopiuj
@Override
public int hashCode() {
return Objects.hash(zmienna3, zmienna4);
}
Wynik:
Kopiuj
Elementów=100000
czas=270
No i wreszcie. jest wynik taki jak trzeba. I całkiem krótki czas.
Dlaczego sam equals nie działa? Bo HashSet najpierw porównuje tylko hashCody, i jak sa różne (a jeśli nie pokryjemy hashCode to mamy duże szanse) to po prostu od razu uznaje obiekty za różne. (Pech)
Dopiero gdy dwa obiekty mają równy hashCode to wtedy jest sprawdzanie equlsem, które o równości przesądza. Można powiedzieć, że hashCode to taki "bardzo zgrubny equals". I zasada jest taka, jeśli obiekty są equals to powinny mieć równy hashCode. Inaczej kiełbasa. (patrz przykład z samym equals).
No dobra. Ale z tego też wynika, że jeśli obiekty są różne wg. equals to wcale nie muszą mieć różnego hashCode! Nie, nie muszą! Różne obiekty nawet często mają ten sam hashCode. No bo to w końcu jeden int. Wiec jak klasa ma dwa pola int... to coś się musi powtórzyć.
Co więc jeśli zrobimy extremalnie zabawny hashCode?:
Kopiuj
@Override
public int hashCode() {
return 42;
}
Czy wyjdzie dobry wynik?
Wyjdzie.
Jaki bedzie czas?
Nie wiem, to się nadal liczy.....
EDIT: właśnie się policzyło.
Kopiuj
Elementów=100000
czas=3005740
Żeby zrozumieć skąd jest taka katastrofa, to trzeba by wiedzieć się jak jest wewnętrznie zorganizowany HashSet (zachęcam do samodzielnego doczytania (w zasadzie o HashMap bo Haset javowy działa w oparciu o HashMap, wtedy będzie jasne).
Widać możliwy skutek nie do końca dobrego hashCode. HashCode powinien się liczyć szybko i dla różnych elementów powinien starać się dawać różne wyniki. Starać się, bo różnych zawsze dać nie może.
Przy okazji "pośredni hashCode" (tylko na jednym polu)
Kopiuj
@Override
public int hashCode() {
return zmienna3.hashCode();
}
Daje całkiem ok wyniki.
Kopiuj
Elementów=100000
czas=151
Dobry wynik i całkiem dobry czas. (bo liczenie hashCodu szybsze).
Akurat u nas tak jak te elementy wsadzaliśmy do hashSet ( jeśli zmienna3 była różna to od razu zmienna4 też była różna).
Tu też widać ważną cechę hashCode. HashCode powinien być dopasowany do tego co i jak wrzucamy (czyli do scenariusza/ biznesu). Automatycznie wygenerowany hashCode często jest nieoptymalny, a nawet dramatycznie zły. (Trzeba mieć pecha, ale są notowane takie przypadki).