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:5 miesięcy
  • Postów:120
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 2 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:prawie 20 lat
  • Ostatnio:dzień
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 2 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.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)