Vodič za hashCode () na Javi

1. Pregled

Hashing je temeljni koncept informatike.

U Javi učinkoviti algoritmi raspršivanja stoje iza nekih od najpopularnijih kolekcija koje imamo na raspolaganju - poput HashMap (za dublji uvid u HashMap, slobodno provjerite ovaj članak) i HashSet.

U ovom ćemo se članku usredotočiti na to kako hashCode () djela, kako se uklapa u zbirke i kako ga pravilno implementirati.

2. Korištenje hashCode () u Strukture podataka

Najjednostavnije operacije na zbirkama mogu biti neučinkovite u određenim situacijama.

Na primjer, ovo pokreće linearno pretraživanje koje je vrlo neučinkovito za popise velikih veličina:

Riječi s popisa = Arrays.asList ("Dobrodošli", "do", "Baeldung"); if (words.contens ("Baeldung")) {System.out.println ("Baeldung je na popisu"); }

Java pruža brojne podatkovne strukture za posebno rješavanje ovog problema - na primjer, nekoliko Karta implementacije sučelja su hash tablice.

Kada koristite hash tablicu, ove zbirke izračunavaju hash vrijednost za zadani ključ pomoću hashCode () metoda i koristite ovu vrijednost interno za pohranu podataka - kako bi operacije pristupa bile mnogo učinkovitije.

3. Razumijevanje kako hashCode () Djela

Jednostavno rečeno, hashCode () vraća cijelu vrijednost, generiranu algoritmom raspršivanja.

Objekti koji su jednaki (prema njihovim jednako ()) mora vratiti isti hash kôd. Nije potrebno da različiti objekti vraćaju različite hash kodove.

Opći ugovor od hashCode () Države:

  • Kad god se na isti objekt poziva više puta tijekom izvršavanja Java aplikacije, hashCode () mora dosljedno vraćati istu vrijednost, pod uvjetom da se ne mijenjaju podaci korišteni u jednakim usporedbama na objektu. Ova vrijednost ne mora ostati dosljedna od jednog izvršenja aplikacije do drugog izvršenja istog programa
  • Ako su dva predmeta jednaka prema jednako (objekt) metodu, a zatim poziva hashCode () metoda na svakom od dva objekta mora proizvesti istu vrijednost
  • Nije potrebno ako su dva objekta nejednaka prema jednako (java.lang.Object) metodu, a zatim poziva hashCode metoda na svakom od dva objekta mora dati različite cjelovite rezultate. Međutim, programeri bi trebali biti svjesni da stvaranje različitih cjelovitih rezultata za nejednake objekte poboljšava izvedbu hash tablica

"Koliko god je to razumno praktično, hashCode () metoda definirana klasom Objekt ne vraća različite cijele brojeve za različite objekte. (To se obično provodi pretvaranjem interne adrese objekta u cijeli broj, ali programska jezik JavaTM ne zahtijeva ovu tehniku ​​implementacije.) "

4. Naivac hashCode () Provedba

Zapravo je sasvim jednostavno imati naivca hashCode () provedba koja se u potpunosti pridržava gore navedenog ugovora.

Da bismo to demonstrirali, definirat ćemo uzorak Korisnik klasa koja poništava zadanu implementaciju metode:

korisnik javne klase {private long id; privatni naziv niza; privatni String e-mail; // standardni getteri / postavljači / konstruktori @Override public int hashCode () {return 1; } @Override javno logičko jednako (Objekt o) {if (this == o) return true; if (o == null) return false; if (this.getClass ()! = o.getClass ()) return false; Korisnik korisnik = (Korisnik) o; return id == user.id && (name.equals (user.name) && email.equals (user.email)); } // ovdje dobivaju i postavljaju}

The Korisnik klasa pruža prilagođene implementacije za oboje jednako () i hashCode () koji se u potpunosti pridržavaju odgovarajućih ugovora. Čak štoviše, nema ništa nelegitimno s imati hashCode () vraćanje bilo koje fiksne vrijednosti.

Međutim, ova implementacija razgrađuje funkcionalnost hash tablica u osnovi na nulu, jer bi svaki objekt bio pohranjen u isti, jedan segment.

U tom se kontekstu traženje hash tablice izvodi linearno i ne daje nam stvarnu prednost - o tome više u odjeljku 7.

5. Poboljšanje hashCode () Provedba

Poboljšajmo malo struju hashCode () provedba uključivanjem svih polja Korisnik klase tako da može proizvesti različite rezultate za nejednake objekte:

@Override public int hashCode () {return (int) id * name.hashCode () * email.hashCode (); }

Ovaj osnovni algoritam raspršivanja definitivno je puno bolji od prethodnog, jer izračunava hash kôd objekta samo množenjem hash kodova Ime i e-mail polja i iskaznica.

Općenito govoreći, možemo reći da je to razumno hashCode () provedba, sve dok držimo jednako () provedba u skladu s tim.

6. Standardno hashCode () Provedbe

Što je bolji algoritam raspršivanja koji koristimo za izračunavanje hash kodova, to će biti bolja izvedba hash tablica.

Pogledajmo "standardnu" implementaciju koja koristi dva prosta broja kako bi dodala još veću jedinstvenost izračunanim hash kodovima:

@Override public int hashCode () {int hash = 7; hash = 31 * hash + (int) id; hash = 31 * hash + (name == null? 0: name.hashCode ()); hash = 31 * hash + (email == null? 0: email.hashCode ()); povratni hash; }

Iako je bitno razumjeti uloge koje hashCode () i jednako () metode igraju, ne moramo ih svaki put implementirati od nule, jer većina IDE-a može generirati prilagođene hashCode () i jednako () implementacije i od Jave 7 dobili smo Objects.hash () korisna metoda za ugodno raspršivanje:

Objects.hash (ime, e-pošta)

IntelliJ IDEA generira sljedeću implementaciju:

@Preuzmi javni int hashCode () {int rezultat = (int) (id ^ (id >>> 32)); rezultat = 31 * rezultat + ime.hashCode (); rezultat = 31 * rezultat + email.hashCode (); povratni rezultat; }

A Eclipse proizvodi ovaj:

@Override public int hashCode () {final int prime = 31; rezultat int = 1; rezultat = glavni * rezultat + ((e-mail == null)? 0: email.hashCode ()); rezultat = prosti * rezultat + (int) (id ^ (id >>> 32)); rezultat = prosti * rezultat + ((ime == null)? 0: name.hashCode ()); povratni rezultat; }

Uz gore spomenute IDE hashCode () implementacije, također je moguće automatski generirati učinkovitu implementaciju, na primjer pomoću Lomboka. U ovom se slučaju mora dodati ovisnost lombok-maven pom.xml:

 org.projectlombok lombok-maven 1.16.18.0 pom 

Sad je dovoljno označiti Korisnik razred sa @EqualsAndHashCode:

Korisnik javne klase @EqualsAndHashCode {// polja i metode ovdje}

Slično tome, ako želimo Apache Commons Lang's HashCodeBuilder razred za generiranje a hashCode () implementacija za nas, ovisnost o zajedničkom jeziku Maven mora biti uključena u pom datoteku:

 commons-lang commons-lang 2.6 

I hashCode () može se implementirati ovako:

korisnik javne klase {public int hashCode () {return new HashCodeBuilder (17, 37). dodati (id). dodati (ime). dodati (e-mail). toHashCode (); }}

Općenito, ne postoji univerzalni recept kojeg bi se trebali pridržavati kada je u pitanju provedba hashCode (). Preporučujemo vam čitanje Joshua Blocha Effective Java, koji sadrži popis temeljnih smjernica za primjenu učinkovitih algoritama raspršivanja.

Ono što se ovdje može primijetiti jest da sve te implementacije koriste broj 31 u nekom obliku - to je zato što 31 ima lijepo svojstvo - njegovo množenje može se zamijeniti bitnim pomakom koji je brži od standardnog množenja:

31 * i == (i << 5) - i

7. Rukovanje sudarima raspršivanja

Suštinsko ponašanje hash tablica postavlja relevantan aspekt ovih struktura podataka: čak i s učinkovitim algoritmom hashiranja, dva ili više objekata mogu imati isti hash kôd, čak i ako su nejednaki. Dakle, njihovi hash kodovi usmjeravali bi na isti segment, iako bi imali različite ključeve hash tablice.

Ova je situacija obično poznata kao hash kolizija, a postoje razne metodologije za njezino rješavanje, pri čemu svaka ima svoje prednosti i nedostatke. Java HashMap koristi zasebnu metodu lanca za rukovanje sudarima:

“Kada dva ili više objekata usmjere na isti segment, oni se jednostavno pohrane na povezanom popisu. U takvom je slučaju tablica raspršivanja niz povezanih popisa, a svaki objekt s istim raspršivanjem dodaje se povezanom popisu na indeksu segmenta u nizu.

U najgorem bi slučaju nekoliko segmenata imalo povezan povezani popis, a preuzimanje objekta s popisa izvodilo bi se linearno.”

Metodologije sudara s hashovima ukratko pokazuju zašto je to tako važno primijeniti hashCode () efikasno.

Java 8 donio je zanimljivo poboljšanje HashMap implementacija - ako veličina segmenta prelazi određeni prag, povezani popis zamjenjuje se mapom stabla. To omogućuje postizanje O (prijava) podigni pogled umjesto pesimističnog Na).

8. Izrada trivijalne aplikacije

Za testiranje funkcionalnosti standarda hashCode () implementaciji, stvorimo jednostavnu Java aplikaciju koja dodaje neke Korisnik prigovara a HashMap i koristi SLF4J za bilježenje poruke na konzolu svaki put kad se metoda pozove.

Evo pristupne točke uzorka aplikacije:

aplikacija javne klase {public static void main (String [] args) {Map users = new HashMap (); Korisnik user1 = novi korisnik (1L, "John", "[e-pošta zaštićena]"); Korisnik user2 = novi korisnik (2L, "Jennifer", "[e-pošta zaštićena]"); Korisnik user3 = novi korisnik (3L, "Mary", "[e-pošta zaštićena]"); users.put (korisnik1, korisnik1); users.put (korisnik2, korisnik2); users.put (user3, user3); if (users.containsKey (user1)) {System.out.print ("Korisnik pronađen u zbirci"); }}} 

A ovo je hashCode () provedba:

javni razred Korisnik {// ... public int hashCode () {int hash = 7; hash = 31 * hash + (int) id; hash = 31 * hash + (name == null? 0: name.hashCode ()); hash = 31 * hash + (email == null? 0: email.hashCode ()); logger.info ("hashCode () pozvan - Izračunato hash:" + hash); povratni hash; }}

Jedini detalj koji vrijedi ovdje naglasiti jest da se svaki put kada se objekt pohrani na hash kartu i provjeri pomoću containsKey () metoda, hashCode () poziva se i izračunati hash kôd ispisuje na konzoli:

[main] INFO com.baeldung.entities.User - hashCode () pozvan - Izračunato hash: 1255477819 [main] INFO com.baeldung.entities.User - hashCode () pozvano - Izračunato hash: -282948472 [main] INFO com.baeldung .entities.User - pozvan hashCode () - Izračunati hash: -1540702691 [glavna] INFO com.baeldung.entities.User - hashCode () pozvan - Izračunati hash: 1255477819 Korisnik pronađen u zbirci

9. Zaključak

Jasno je da je proizvodnja učinkovita hashCode () implementacije često zahtijevaju mješavinu nekoliko matematičkih pojmova (tj. prostih i proizvoljnih brojeva), logičkih i osnovnih matematičkih operacija.

Bez obzira na to, potpuno je moguće implementirati hashCode () učinkovito, a da se uopće ne pribjegava ovim tehnikama, sve dok osiguravamo da algoritam raspršivanja proizvodi različite hash kodove za nejednake objekte i da je u skladu s provedbom jednako ().

Kao i uvijek, svi primjeri koda prikazani u ovom članku dostupni su na GitHubu.