Przesłonięcie metod equals i hashCode

Przesłonięcie metod equals i hashCode
RU
  • Rejestracja:ponad 12 lat
  • Ostatnio:około 5 lat
  • Postów:211
0

Cześć

Używałem wcześniej kolekcji w javie ale nie przykładałem wagi do nadpisywania metod equals i hashCode. Doczytałem o kontrakcie między tymi metodami ale nie wiem jak miał by wyglądać przypadek gdzie brak tych metod wywołał by błąd w założeniach, np. przy mapach.

Kopiuj
public class T2 
{
	public String zmienna3;
	public String zmienna4;
	public static void main(String[] args) 
	{
		T2 t2 = new T2();
		t2.zmienna3 = "test";
		t2.zmienna4 = "test";
		if(t2.zmienna3.hashCode() == t2.zmienna4.hashCode())
		{
			System.out.println("równe");
		}else
		{
			System.out.println("Nierówne");
		}
	}
}

W tym przykładzie przy obu metodach dostaję wartość true. Wiem, że lepiej było by to zobrazować na kolekcjach ale kiedy i używałem to wszystko mi działało tak jak chciałem.
Jak wyglądał by przykład gdzie brak przesłonięcia tych metod spowodował by błąd (wiem, że nie chodziu tu o błąd kompilacji tylko o to, że np. dostanę dwa rekordy w kolekcji gdzie nie powinno być powtórzeń)?

  • Rejestracja:prawie 8 lat
  • Ostatnio:14 dni
  • Postów:121
0

Stwórz drugi obiekt typu T2 i nadaj zmiennym te same wartości. Nie porównuj hashcode zmiennych tylko obiektów typu T2. Potem dodaj metodę hashcode i zobacz różnicę :)

jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
3

Ż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).


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 5x, ostatnio: jarekr000000
Wibowit
  • Rejestracja:około 20 lat
  • Ostatnio:około 11 godzin
1

Ja jeszcze dodam bardzo ważną sprawę. Poniższa metoda zawsze zwróci true:

Kopiuj
boolean metoda() {
  String x = "aaa";
  String y = "aaa";
  return x == y;
}

Dzieje się tak dlatego, że wszystkie literały (wartości wprost) z klasy lądują w puli stałych (czyli w sekcji pliku .class) i te na etapie kompilacji są deduplikowane. Kompilator sprawdzi sobie, że "aaa" i "aaa" to te same Stringi, więc zapisze tylko jeden i tylko jeden potem będzie używany w czasie wykonywania. Jeśli chcesz ominąć tą deduplikację w czasie kompilacji to niezawodnym rozwiązaniem jest zrobienie łączenia stringów na etapie wykonania, np:

Kopiuj
boolean metoda(String param) {
  String a = "a" + param;
  String b = "a" + param;
  return a == b;
}

Przykład: https://www.ideone.com/9yPLOd

Możesz sobie poguglać "java constant pool". Przykładowa strona z wyjaśnieniami: https://stackoverflow.com/q/10209952

Kolejna sprawa:
Przesłonięcie, a nadpisanie metody to dwie zupełnie różne rzeczy.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 2x, ostatnio: Wibowit
RU
  • Rejestracja:ponad 12 lat
  • Ostatnio:około 5 lat
  • Postów:211
0

Bardzo dziękuję za obszerną odpowiedź.
Dodałem napisany przez Ciebie kod i zwraca mi:

Kopiuj
Elementów=1
czas=92

Mój kod:

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 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);
    }
	
    public int hashCode() {
        return Objects.hash(zmienna3, zmienna4);
    }
    
	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= " + 1, "y= " + 1);
				mySet.add(value);
			}
		}
		return mySet.size();
	}
}

korzystam z Eclipsa i kiedy chcę wygenerować te metody to wyglądają zupełnie inaczej niż te od Ciebie:

Kopiuj
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((zmienna3 == null) ? 0 : zmienna3.hashCode());
		result = prime * result + ((zmienna4 == null) ? 0 : zmienna4.hashCode());
		return result;
	}
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		T2 other = (T2) obj;
		if (zmienna3 == null) {
			if (other.zmienna3 != null)
				return false;
		} else if (!zmienna3.equals(other.zmienna3))
			return false;
		if (zmienna4 == null) {
			if (other.zmienna4 != null)
				return false;
		} else if (!zmienna4.equals(other.zmienna4))
			return false;
		return true;
	}

Czy to kwestia narzędzia, którego używam?

jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
1

To mi zabiłeś ćwieka... ale na szczęście cuda tak często się nie zdarzają:
Masz taką linijkę:

Kopiuj
 final T2 value = new T2("x= " + 1, "y= " + 1);

zamiast

Kopiuj
 final T2 value = new T2("x= " + i, "y= " + i);

Wrzucałeś zawsze jeden i ten sam T2 (x=1, y=1).

Co do różnych implementacji hashCode i equals przez rózne IDE - nie ma to większego znaczenia. Co do działania są takie same.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
RU
Dziękuję za odpowiedź. Sam bym siedział nad tym pewnie godzinami.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.