Guide till hashCode () i Java

1. Översikt

Hashing är ett grundläggande begrepp inom datavetenskap.

I Java står effektiva hashingalgoritmer bakom några av de mest populära samlingarna vi har tillgängliga - till exempel HashMap (för en fördjupad titt på HashMap , kolla gärna den här artikeln) och HashSet.

I den här artikeln fokuserar vi på hur hashCode () fungerar, hur det spelar in i samlingar och hur man implementerar det korrekt.

2. Användning av hashCode () i datastrukturer

De enklaste operationerna på samlingar kan vara ineffektiva i vissa situationer.

Till exempel utlöser detta en linjär sökning som är mycket ineffektiv för listor med stora storlekar:

List words = Arrays.asList("Welcome", "to", "Baeldung"); if (words.contains("Baeldung")) { System.out.println("Baeldung is in the list"); }

Java tillhandahåller ett antal datastrukturer för att hantera denna fråga specifikt - till exempel flera Karta gränssnitt implementeringar är hashtabeller.

När du använder en hashtabell, dessa samlingar beräkna hash-värdet för en given nyckel med hashkod () metod och använda detta värde internt för att lagra data - så att accessverksamheten är mycket effektivare.

3. Förstå hur hashCode () fungerar

Enkelt uttryckt returnerar hashCode () ett heltal, genererat av en hashingalgoritm.

Objekt som är lika (enligt deras lika () ) måste returnera samma hash-kod. Det krävs inte att olika objekt returnerar olika hashkoder.

I det allmänna avtalet för hashCode () anges:

  • När det anropas på samma objekt mer än en gång under en Java-applikation, måste hashCode () konsekvent returnera samma värde, förutsatt att ingen information som används i jämförelser av objektet ändras. Det här värdet behöver inte förbli konsekvent från en körning av en applikation till en annan körning av samma applikation
  • Om två objekt är lika enligt metoden lika (Objekt) måste anropa hashCode () -metoden på vart och ett av de två objekten producera samma värde
  • Det krävs inte att om två objekt är ojämlika enligt metoden lika (java.lang.Object) , måste anropa hashCode- metoden på vart och ett av de två objekten ge distinkta heltalresultat. Utvecklare bör dock vara medvetna om att det att producera distinkta heltalresultat för ojämna objekt förbättrar hashtabellernas prestanda

”Så mycket som är rimligt praktiskt returnerar hashCode () -metoden som definieras av klass Object distinkta heltal för distinkta objekt. (Detta implementeras vanligtvis genom att konvertera objektets interna adress till ett heltal, men denna implementeringsteknik krävs inte av JavaTM-programmeringsspråket.) ”

4. En Naiv hashCode () Implementering

Det är faktiskt ganska enkelt att ha en naiv hashCode () -implementering som helt följer ovanstående kontrakt.

För att visa detta, kommer vi att definiera ett prov Användar klass som åsido metoden standard genomförande:

public class User { private long id; private String name; private String email; // standard getters/setters/constructors @Override public int hashCode() { return 1; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null) return false; if (this.getClass() != o.getClass()) return false; User user = (User) o; return id == user.id && (name.equals(user.name) && email.equals(user.email)); } // getters and setters here }

I Användar klassen ger anpassade implementeringar för båda equals () och hashkod () som till fullo ansluta sig till respektive kontrakt. Ännu mer finns det inget olagligt med att hashCode () returnerar något fast värde.

Denna implementering försämrar dock funktionerna för hash-tabeller till i princip noll, eftersom varje objekt skulle lagras i samma enda hink.

I detta sammanhang utförs en hashtabelluppsättning linjärt och ger oss ingen verklig fördel - mer om detta i avsnitt 7.

5. Förbättring av hashkod () Genomförande

Låt oss förbättra lite aktuell hashkod () genomförande genom att inkludera alla områden av användar klassen så att den kan ge olika resultat för ojämlika objekt:

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

Denna grundläggande hashingalgoritm är definitivt mycket bättre än den tidigare, eftersom den beräknar objektets hashkod genom att bara multiplicera hashkoderna för namn och e- postfält och id .

Generellt sett kan vi säga att detta är en rimlig implementering av hashCode () , så länge vi håller lika () implementeringen i linje med den.

6. Standard hashCode () implementeringar

Ju bättre hashingalgoritmen som vi använder för att beräkna hashkoder, desto bättre blir hashtabellernas prestanda.

Låt oss ta en titt på en "standard" -implementering som använder två primtal för att lägga till ännu mer unikhet i beräknade hashkoder:

@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()); return hash; }

Även om det är viktigt att förstå de roller som hashCode () och equals () -metoder spelar, behöver vi inte implementera dem från grunden varje gång, eftersom de flesta IDE kan generera anpassade hashCode () och lika () implementeringar och sedan Java 7, vi har en Objects.hash () -metod för bekväm hashing:

Objects.hash(name, email)

IntelliJ IDEA genererar följande implementering:

@Override public int hashCode() { int result = (int) (id ^ (id >>> 32)); result = 31 * result + name.hashCode(); result = 31 * result + email.hashCode(); return result; }

Och Eclipse producerar den här:

@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((email == null) ? 0 : email.hashCode()); result = prime * result + (int) (id ^ (id >>> 32)); result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; }

Förutom ovanstående IDE-baserade hashCode () -implementeringar är det också möjligt att automatiskt generera en effektiv implementering, till exempel med Lombok. I det här fallet måste lombok-maven-beroendet läggas till pom.xml :

 org.projectlombok lombok-maven 1.16.18.0 pom 

Det är nu tillräckligt för att kommentera den Användaren klass med @EqualsAndHashCode :

@EqualsAndHashCode public class User { // fields and methods here }

På samma sätt, om vi vill att Apache Commons Langs HashCodeBuilder- klass ska generera en hashCode () -implementering för oss, måste commons-lang Maven-beroendet inkluderas i pom-filen:

 commons-lang commons-lang 2.6 

Och hashCode () kan implementeras så här:

public class User { public int hashCode() { return new HashCodeBuilder(17, 37). append(id). append(name). append(email). toHashCode(); } }

I allmänhet finns det inget universellt recept att hålla fast vid när det gäller implementering av hashCode () . Vi rekommenderar starkt att du läser Joshua Blochs effektiva Java, som ger en lista med grundliga riktlinjer för implementering av effektiva hashingalgoritmer.

Vad som kan märkas här är att alla dessa implementeringar använder nummer 31 i någon form - detta beror på att 31 har en fin egenskap - dess multiplikation kan ersättas med en bitvis förskjutning som är snabbare än standardmultiplikationen:

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

7. Hantering av haschkollisioner

Det inneboende beteendet hos hashtabeller ökar en relevant aspekt av dessa datastrukturer: även med en effektiv hashingalgoritm kan två eller flera objekt ha samma hashkod, även om de är ojämna. Så deras hashkoder skulle peka på samma hink, även om de skulle ha olika hashtabellnycklar.

Denna situation är allmänt känd som en hashkollision, och olika metoder finns för att hantera den, var och en har sina för- och nackdelar. Java's HashMap använder den separata kedjningsmetoden för hantering av kollisioner:

”När två eller flera objekt pekar på samma hink lagras de helt enkelt i en länkad lista. I ett sådant fall är hashtabellen en matris med länkade listor, och varje objekt med samma hash läggs till i den länkade listan vid skopindex i matrisen.

I värsta fall skulle flera skopor ha en länkad lista bunden till sig och hämtningen av ett objekt i listan skulle utföras linjärt . ”

Metoder för haschkollision visar i ett nötskal varför det är så viktigt att implementera hashCode () effektivt .

Java 8 gav en intressant förbättring av HashMap- implementeringen - om en skopstorlek överskrider det vissa tröskeln ersätts den länkade listan med en trädkarta. Detta gör det möjligt att uppnå O ( logn ) slå upp istället för pessimistisk O (n) .

8. Skapa en ny applikation

För att testa funktionaliteten hos en vanlig hashkod () genomförande, låt oss skapa en enkel Java-program som lägger till några användarobjekt till en HashMap och använder SLF4J för inloggning ett meddelande till konsolen varje gång Metoden kallas.

Här är exempelapplikationens startpunkt:

public class Application { public static void main(String[] args) { Map users = new HashMap(); User user1 = new User(1L, "John", "[email protected]"); User user2 = new User(2L, "Jennifer", "[email protected]"); User user3 = new User(3L, "Mary", "[email protected]"); users.put(user1, user1); users.put(user2, user2); users.put(user3, user3); if (users.containsKey(user1)) { System.out.print("User found in the collection"); } } } 

Och det här är implementeringen av hashCode () :

public class User { // ... 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() called - Computed hash: " + hash); return hash; } }

Den enda detalj som är värt att betona här är att varje gång ett objekt lagras på hashkartan och kontrolleras med den innehållerKey () -metoden, anropas hashCode () och den beräknade hashkoden skrivs ut till konsolen:

[main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819 [main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: -282948472 [main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: -1540702691 [main] INFO com.baeldung.entities.User - hashCode() called - Computed hash: 1255477819 User found in the collection

9. Slutsats

Det är uppenbart att att producera effektiva hashCode () -implementeringar ofta kräver en blandning av några matematiska begrepp, (dvs. primära och godtyckliga tal), logiska och grundläggande matematiska operationer.

Oavsett är det helt möjligt att implementera hashCode () effektivt utan att använda dessa tekniker alls, så länge vi ser till att hashingalgoritmen producerar olika hashkoder för ojämna objekt och överensstämmer med implementeringen av lika () .

Som alltid finns alla kodexempel som visas i den här artikeln tillgängliga på GitHub.