Dlaczego funkcja hashCode() jest świętym Graalem rekrutierów Javy ?

0

Byłem dzisiaj na rozmowie o pracę i to chyba kolejna ( 3 jak liczę ) na której mnie o to pytano. Zastanawia mnie skąd taka ważność / upodobanie sobie rekruterów technicznych tej funkcji, bo z osobistego doświadczenia nie używałem jej ani nie musiałem jej nadpisywać ani razu ( przynajmniej explicite jak na razie) ?

0

bo to sa podstawy podstaw a (wg moich szacunkow ;)) 80% javowcow nie radzi sobie z wytlumaczeniem jak to dziala.

0

Nie trzeba explicite, skoro lombok może ją wygenerować

2

Jeśli ktoś nie rozumie co robi i jak działa hashcode+equals to prowadzi to do trudnych do zdebugowania błędów. Poza tym jest to dobre pytanie żeby sprawdzić czy kandydat kiedykolwiek zastanawiał się nad tym "jak to w ogóle działa", czy tylko bezrefleksyjnie bierze coś "na wiarę" albo zakłada że jest tam jakaś "magia" :)

0

... ale to nic nie sprawdza. To jedno z najczestszych pytan w polaczeniu z cyzm rozni sie interfejs od klasy abstrakcyjnej.

0
WhiteLightning napisał(a):

... ale to nic nie sprawdza. To jedno z najczestszych pytan w polaczeniu z cyzm rozni sie interfejs od klasy abstrakcyjnej.

Może sprawdza, może nie -- ale jedno i drugie trzeba wiedzieć.

0

... ale to nic nie sprawdza.

jesli jest to pytanie na ktore trzeba odpowiedziec jednym zdaniem wg klucza rekruterow to rzeczywiscie troche bieda. jesli natomiast jest to w formie rozmowy w ktorej kandydat moze sie wykazac wiedza bo rekruterzy sa dociekliwi to czemu nie?
nie twierdze ze to regula ale nie spotkalam sie jeszcze zeby kandydat umial dobrze wytlumaczyc jak dziala hashcode/equals, alternatywy dla nich i np gotchas przy implementacji i byl oprocz tego kiepski, wiec cos w tym jest :D

0

Wystarczy jak powiem rekruterowi, że jak nie zaimplementuje funkcji hashCode() to będę miał lipę w HashSecie, HashMapie itd ?

0
WhiteLightning napisał(a):

... ale to nic nie sprawdza. To jedno z najczestszych pytan w polaczeniu z cyzm rozni sie interfejs od klasy abstrakcyjnej.

Zawsze można spytać sie o przechodność (a eq b i b eq c => a eq c) i odwrotność czy jak to sie nazywa (w sensie jesli a eq b to b eq a) + Liskov Substitution Principle i jest git

@slayer9 a dlaczego miałbyś mieć?

0

@slayer9 a dlaczego miałbyś mieć?

Jak mam HashSet Product'ów i klasa Product nie ma metody hashCode(), to nie będą one w tym HashSecie uporządkowane tak jak powinny. Poza tym jak sprawdzam czy HashSet zawiera już dany Product to odpowiedź uzyskam dzięki temu, że zostaną porównane ich hashcody.

0

Ciekawym pomysłem jest pokazanie, że będzie lipa jak się użyje wygenerowanego przez lombok lub IDE hashCode. W typowym kodzie biznesowym to się zdarzy raz na 10 lat doświadczenia, ale może.

0
jarekr000000 napisał(a):

Ciekawym pomysłem jest pokazanie, że będzie lipa jak się użyje wygenerowanego przez lombok lub IDE hashCode. W typowym kodzie biznesowym to się zdarzy raz na 10 lat doświadczenia, ale może.

Ale dlaczego będzie lipa? Bo za null podstawia stałą?

0

@wartek01: chociażby relacje dwukierunkowe wysyłają lomboka na drzewo, bo ten produkuje stackOverflow

1

Pytanie w związku z tą funkcją, w jakim przypadku chcemy napisać własną wersję hashCode() ? Czy ma to związek z tym, że dla niektórych argumentów dochodzi do klastrowania (konfliktów hash-y ?) się wartości i pogarsza się czas dostępu o elementów składowanych w HashMap-ie ? Nigdy nie miałem potrzeby pisać swoją wersje tej metody, więc czy ktoś może podać mi jakiś przykład problemu, gdzie standardowa implementacja hashCode() nie zdaje egzaminu ?

0

Piszemy ją kiedy chcemy przesłonić equalsa :)

0

Piszemy ją kiedy chcemy przesłonić equalsa :)

To rozumiem, że equals i hashCode() są ze sobą nierozerwalnie związane, tylko nie rozumiem kiedy standardowy equals może przestać wystarczać, a co za tym idzie hashCode().

1

Standardowy equals porównuje referencje. Np. jak masz LocalDate i jeśli utworzysz 2 osobne instancje ale mające ten sam dzień, miesiąc i rok to gdyby zachowywało się zgodnie z defaultową implentacja zwracałoby false, ale jest to metoda przeciążona to zwróci true. Generalnie equal jest używany gdy mamy jakieś obiekty które reprezentują dane np. właśnie LocalDate czy np. Point
zwłascza gdy implementujemy Value Object Pattern (czyli obiekty niemutowalne prezentujące dane)

1
TwójJanuszBrzmiZnajomo napisał(a):

Byłem dzisiaj na rozmowie o pracę i to chyba kolejna ( 3 jak liczę ) na której mnie o to pytano. Zastanawia mnie skąd taka ważność / upodobanie sobie rekruterów technicznych tej funkcji, bo z osobistego doświadczenia nie używałem jej ani nie musiałem jej nadpisywać ani razu ( przynajmniej explicite jak na razie) ?

No to zajebiście, skoro wykryłeś że to konik rekruterów, to tylko się nauczyć, zamiast płakac na forum.

BTW, taka ciekawostka: https://stackoverflow.com/questions/1381060/hashcode-uniqueness

2

Standardowy equals porównuje tożsamość, a nie zawartość. Dwa obiekty o takiej samej zawartości będą uznawane za różne przy standardowym equalsie. Standardowy hashCode natomiast z praktycznego punktu widzenia jest wartością pseudolosową, więc z dużą dozą prawdopodobieństwa dwa obiekty o takiej samej zawartości będą miały różne standardowe hashCode'y.

Nadpisanie equals i hashCode jest niezbędne, by móc sensownie używać klasy jako klucza w HashMapie. Bardzo często podczas używania mapy tworzonych jest wiele kluczy o identycznej zawartości. Bez equals i hashCode HashMapa nie działa prawidłowo. Przykład https://www.ideone.com/ew7u9C :

import java.util.HashMap;
import java.util.Map;
 
class Main {
    static class Klucz {
        private final String value;
 
        Klucz(String value) {
            this.value = value;
        }
 
        @Override
        public String toString() {
            return "Klucz(" + value + ")";
        }
    }
 
    public static void main(String[] args) {
        Map<Klucz, Integer> mapa = new HashMap<>();
        mapa.put(new Klucz("coś"), 5); // np tu klucz z raportu
        mapa.put(new Klucz("coś"), 8); // a tu klucz z bazy danych
        System.out.println(mapa);
    }
}

Wynik to: {Klucz(coś)=5, Klucz(coś)=8}
Mamy w HashMapie dwa klucze o takiej samej zawartości, bo nie nadpisaliśmy equalsa i hashCode'a. Nadpisanie samego equalsa też nie pomoże.

Jeżeli nie obchodzi nas wydajność, a jedynie poprawność to można nadpisać hashCode w następujący sposób:

        @Override
        public int hashCode() {
            return 1;
        }

To spowoduje, że przy użyciu klasy z takiem hashCodem HashMapa zdegraduje się (pod względem efektywności) do LinkedListy z większym narzutem.

0

Co do komentarza wyżej poza tym że sie zgodze, to jeszcze dodam że Mapy powinny miec niemutowalne obiekty jako klucze, bo inaczej igra sie z ogniem :)

0

Bo na pytanie "jakie wzorce projektowe pan(i) zna?" pada radosne "singleton".

1
    Test A = new Test("A","B");
    Test B = new Test("A","B");

    System.out.println(A.equals(B));


    Integer integerOne = new Integer(12345);
    Integer integerTwo = new Integer(12345);

    System.out.println(integerOne.equals(integerTwo));

    System.out.println("1 :\n" + A.hashCode());
    System.out.println("2 :\n" + B.hashCode());

    System.out.println("3 :\n" + integerOne.hashCode());
    System.out.println("4 :\n" + integerTwo.hashCode());

false
true
1 :
2133927002
2 :
1836019240
3 :
12345
4 :
12345

Why ?

1

Metoda equals w Integerze jest nadpisana i ma nawet własną dokumentację

Compares this object to the specified object. The result is true if and only if the argument is not null and is an Integer object that contains the same int value as this object.

Metoda hashCode w Integerze też jest nadpisana i też ma własną dokumentację

Returns: a hash code value for this object, equal to the primitive int value represented by this Integer object.

JavaDoc nie gryzie.

Natomiast to co zwrócą metody equals i hashCode w klasie Test zależy od tego jak są tam zaimplementowane. Jeśli klasa Test, ani żadna nadklasa nie nadpisuje tych metod to będziesz miał standardowe implementacje z Objecta.

0

https://www.sitepoint.com/how-to-implement-javas-hashcode-correctly/

Czyli o ile dobrze rozumiem, poprzez nadpisanie metody hashCode() swoją wersją bierzemy pod uwagę wartości składowe bardziej złożonych bytów np. nie wrapperów. W ten sposób możemy ustalić własną definicję "równości" 2 obiektów podczas gdy domyślna implementacja z klasy Object razem z equals() porównuje tylko referencje.

0

@Olamagato:

hashCode(), to funkcja, która na podstawie wartości obiektu swojego typu zwraca liczbę całkowitą. Różne obiekty różnych typów mogą zwracać tę samą wartość, ale dwa różne obiekty tego samego typu najlepiej powinny dawać unikalne wartości (nie powtarzające się).

Teraz - do czego to służy?
Skoro różne wartości obiektów dają zawsze różne wartości, to można te wartości wykorzystać jako indeks bardzo szybkiej tablicy referencji obiektów. Dzięki takiemu podejściu teoretyczny koszt dostępu do obiektu to zawsze jedno dodawanie. Tak właśnie działa hashMap.
Oczywiście tworzenie tablic o zakresie indeksów -2mld..+2mld (32-bit) byłoby bezsensownym marnotrawstwem pamięci, więc tworzy się znacznie mniejszą tablicę (np. 8192 elementów), w której indeksy zawija się modulo jej rozmiar. Zwykle rozmiar jest potęgą dwójki, co redukuje modulo (resztę z dzielenia) do znacznie szybszej operacji binarnej AND. "Zawinięte" indeksy oraz powtarzające się indeksy różnych typów obiektów powodują, że pod ten sam indeks trafią różne obiekty (również tego samego typu). A to powoduje, że wartościami komórek tej tablicy nie mogą być referencje tych obiektów, ale listy tych obiektów o powtarzającym się "zawiniętym" hashCode. Powoduje to konieczność wyszukania na takiej liście właściwego obiektu, o który nam chodziło. I właśnie w tym miejscu potrzebne jest poprawne zdefiniowanie funkcji equal, która pozwoli wyszukać liniowo właściwy obiekt przez porównanie z szukanym wzorcem. Oczywiście kiedy dodajemy do mapy nie potrzebujemy niczego wyszukiwać, więc tylko dodajemy obiekt (referencję) do listy, co też jest bardzo szybkie.

Tak więc cała operacja na hash mapie sprowadza się do wyliczenia wartości hashCode, jednego dodawania dla wyliczenia indeksu, jednej operacji AND oraz, albo wyszukania obiektu na kilkuelementowej liście (często zdegenerowanej do jednego elementu), albo dodania do listy (często pustej). To właśnie dlatego hash mapa jest tak niesamowicie szybka i ma średnio stały koszt czasu dodawania i wyciągania obiektów.
A to wszystko opiera się tylko na koncepcji kodu hash.
Jedynym wartym uwagi kosztem jest czas obliczania samego kodu - dlatego ważne jest, żeby wyliczać go dobrze i wydajnie.

0

Dobra chyba trochę się tu namieszało. Możemy (i często powinniśmy) nadpisać metody hashCode() i equals() tworząc nową klasę.
Wymagania formalne są bardzo proste:
hash code musi być stabilny - ten sam obiekt, pozostając w tym samym stanie, powinien zwracać stały hash code
Jeżeli obiekty zwracające różne wartości hashCode() muszą zwracać false z przyrównania metodą equals()
I tyle formalności, teraz praktyka z hashmapą i wstawianiem 10k obiektów, implementacja w Kotlinie (po zmianie pracy mogę sobie pisać w normalnym języku tylko w weekendy):

Obiekt klucza zwracający zawsze identyczny hash:

class Key(var obj: Int){

    override fun hashCode(): Int {
        return 1
    }

    override fun equals(other: Any?): Boolean {
        return obj.equals(other)
    }
}

Wyniki:

Starting ...
Inserted 10000 0.814687905
Inserted 20000 2.326663591
Inserted 30000 4.228907382
Inserted 40000 7.34869445
Inserted 50000 13.435786975
Inserted 60000 21.295969176
Inserted 70000 28.27743323
Inserted 80000 36.046472472
Inserted 90000 43.750796672
Inserted 100000 51.51927041
Insertion Time [s]: 209.044751639
Keys count: 100000

Po zmianie implementacji hashcode na nieco bardziej sensowną:

class Key(var obj: Int){

    override fun hashCode(): Int {
        return obj
    }

    override fun equals(other: Any?): Boolean {
        return obj.equals(other)
    }
}

Wyniki są nieco inne:

Starting ...
Inserted 10000 0.006125059
Inserted 20000 0.002564865
Inserted 30000 0.001864705
Inserted 40000 0.001031681
Inserted 50000 0.001496577
Inserted 60000 0.001554688
Inserted 70000 0.001227009
Inserted 80000 6.94528E-4
Inserted 90000 4.44161E-4
Inserted 100000 0.00104704
Insertion Time [s]: 0.018119177
Keys count: 100000

Co ważne - obie implementacje są formalnie poprawne obiekty się dodają, można po nich wyszukiwać, mapa zawiera właściwą liczbę wpisów różnica jest wyłącznie w złożoności obliczeniowej. Przy wyciąganiu obiektów z mapy będzie to już trochę bardziej litościwe, z grubsza i średnio: O(n/2) vs O(log(n)) - jak ktoś chce niech sobie to sprawdzi.
Pełna implementacja testu:

fun main(args: Array<String>) {
    println("Starting ...")

    val operationStart = System.nanoTime()

    val map = HashMap<Key, String>()
    val value = "Some string"
    val epoch = 1E4.toInt()

    var epochStart = operationStart

    (1..1E5.toInt()).forEach{
        map.put(Key(it), value)
        if((it % epoch) == 0){
            val nanoTime = System.nanoTime()
            println("Inserted ${it} ${duration(epochStart, nanoTime)}")
            epochStart = nanoTime
        }
    }

    val insertionEndTime = System.nanoTime()

    println("Insertion Time [s]: ${duration(operationStart, System.nanoTime())}")
    println("Keys count: ${map.keys.size}")
}

fun duration(start: Long, end: Long): Double {
    return (end - start) / 1E9
}

class Key(var obj: Int){

    override fun hashCode(): Int {
        return obj
    }

    override fun equals(other: Any?): Boolean {
        return obj.equals((other as? Key)?.obj)
    }
}

Efektywna implementacja hashCode() sprowadza się do możliwie szerokiego wykorzystania zakresu zwracanej wartości - tak aby występowało możliwie mało konfliktów hashCode() pomiędzy obiektami zwracającymi false z metody equals().
PS. Chyba też zacznę o to pytać :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1