Hashcodes und Hashfunktionen
Kurz gesagt: Ein Hashcode ist eine Zahl, die hashCode() aus dem Schlüssel berechnet; HashMap wählt damit den Bucket, in dem es sucht, und findet so einen Schlüssel auch unter einer Million schnell.
Problem
Der Platz wurde zweimal verkauft
Die Theaterkasse hält die verkauften Plätze in einer Map: Platz → Name des Zuschauers. Ein Platz ist eine Klasse mit Reihe und Nummer:
class Seat { int row; int number;
Seat(int row, int number) { this.row = row; this.number = number; }}Map<Seat, String> bookings = new HashMap<>();bookings.put(new Seat(5, 12), "Maria");
System.out.println(bookings.get(new Seat(5, 12)));System.out.println(bookings.size());bookings.put(new Seat(5, 12), "Jonas");System.out.println(bookings.size());null12Maria hat Platz 5/12 gekauft, aber get mit demselben Platz liefert null. Dann kauft Jonas denselben Platz, und die Map hat jetzt zwei Schlüssel 5/12. Warum?
Die Idee
Hashcode: eine Zahl aus dem Schlüssel
Eine HashMap vergleicht den Schlüssel nicht mit allen anderen. Sie berechnet aus dem Schlüssel eine ganze Zahl und weiß daraus, wo sie suchen muss. Diese Zahl heißt Hashcode, und die Methode hashCode() gibt sie zurück. Jedes Objekt hat sie, denn sie steht in Object.
System.out.println("Maria".hashCode());System.out.println("Maria".hashCode());System.out.println("Marie".hashCode());System.out.println("maria".hashCode());741137507411375074113754103666422- Derselbe Text gibt dieselbe Zahl, jedes Mal und auf jedem Computer.
- Eine kleine Änderung gibt eine andere Zahl: „Maria“, „Marie“ und „maria“ sind drei verschiedene Hashcodes.
- Ein Hashcode ist schnell berechnet, egal wie viele Schlüssel in der Map sind.
Die Methode, die die Zahl macht, heißt Hashfunktion.
Genauer, für Neugierige
- Für
Stringist die Formel in der Java-Dokumentation festgelegt:s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1], wobeis[i]der Code jedes Zeichens ist. Das Ergebnis ist einint, deshalb wird es beim Überlauf negativ.
- Kryptografische Hashfunktionen wie SHA-256 sind etwas anderes. Sie geben ein langes Ergebnis, und es ist praktisch unmöglich, zwei Texte mit demselben Hash zu finden. Deshalb prüft man mit ihnen, ob eine Datei nicht verändert wurde, und speichert damit Passwörter.
hashCode()ist für schnelles Suchen da und gibt keinen solchen Schutz.
Sieh es dir an
Die Buckets
Innen ist eine HashMap ein Array von Buckets. Die Nummer des Buckets für einen Schlüssel ist der Rest des Hashs bei der Division durch die Anzahl der Buckets:
String[] keys = { "Laura", "Jonas", "Tom", "Finn" };int buckets = 8;for (String key : keys) { int hash = key.hashCode(); int index = Math.floorMod(hash, buckets); System.out.println(key + " -> " + hash + " -> Bucket " + index);}Schritt für Schritt, in Worten
- Vier Schlüssel und acht leere Buckets, nummeriert von 0 bis 7.
- "Laura".hashCode() ist 73193391. Der Rest bei der Division durch 8 ist 7: Das ist der Bucket.
- "Jonas" ergibt 71756187, Bucket 3.
- "Tom" ergibt 84274, Bucket 2.
- "Finn" landet auch in Bucket 3: eine Kollision. Die beiden Schlüssel sind in einem Bucket: "Finn" hängt wie in einer Kette hinter "Jonas".
- Suche nach "Finn": dieselbe Zahl, derselbe Bucket 3. equals mit "Jonas": nein. Mit "Finn": ja.
putberechnet den Bucket und legt das Paar dort ab.getberechnet denselben Bucket und sucht nur darin. Deshalb ist es auch mit einer Million Schlüssel schnell.Math.floorModist ein Rest der Division, der nie negativ ist. Ein Hashcode kann negativ sein, eine Bucket-Nummer nicht.- „Jonas“ und „Finn“ sind in Bucket 3 gelandet. Das heißt Kollision. Darum geht es in der nächsten Karte.
Genauer, für Neugierige
- Die echte
HashMapberechnet den Index etwas anders:(h ^ (h >>> 16)) & (n - 1). Die Anzahl der Bucketsnist immer eine Zweierpotenz, und& (n - 1)ist ein schneller Rest der Division durchn. Die Zahlen sind dort also andere, aber das Prinzip ist dasselbe.
Probier es aus
Welcher Bucket?
Der Hashcode eines Integer ist die Zahl selbst. Die Bestellnummern kommen in eine Map mit 4 Buckets.
Was gibt das Programm aus?
int[] orderIds = { 6, 10, 13, 21 };for (int id : orderIds) { Integer key = id; System.out.println(id + " -> Bucket " + Math.floorMod(key.hashCode(), 4));}Die Reste bei der Division durch 4: 6 und 10 geben 2, 13 und 21 geben 1. Vier Schlüssel, zwei Kollisionen, zwei leere Buckets. Mit mehr Buckets verteilen sich die Schlüssel gleichmäßiger.
Die Idee
Kollision: hashCode findet den Bucket, equals findet den Schlüssel
Verschiedene Schlüssel können denselben Hashcode haben. Die zwei bekanntesten Beispiele:
System.out.println("Aa".hashCode() + " " + "BB".hashCode());
Map<String, Integer> codes = new HashMap<>();codes.put("Aa", 1);codes.put("BB", 2);System.out.println(codes.get("Aa") + " " + codes.get("BB"));2112 21121 2Die beiden Schlüssel sind in einem Bucket, aber die Map verwechselt sie nicht. Der Bucket hält eine kleine verkettete Liste von Paaren. get geht sie entlang und vergleicht mit equals, bis es den Schlüssel findet.
Die zwei Schritte arbeiten also so:
hashCode()sagt, in welchem Bucket gesucht wird.equals()sagt, welcher der Schlüssel im Bucket der gesuchte ist.
Jetzt ist klar, was mit dem Theaterplatz passiert ist. Seat überschreibt keine der beiden Methoden, also nutzt es die Versionen aus Object. Dort ist equals dasselbe wie ==, und hashCode ist meist für jedes Objekt anders. Die beiden Objekte new Seat(5, 12) sind zwei verschiedene Objekte, also auch zwei verschiedene Schlüssel.
Die Idee
Der Vertrag: equals und hashCode zusammen
Seat muss sagen, wann zwei Plätze derselbe Platz sind, und gleichen Plätzen denselben Hashcode geben:
class Seat { int row; int number;
Seat(int row, int number) { this.row = row; this.number = number; }
@Override public boolean equals(Object other) { if (!(other instanceof Seat)) { return false; } Seat s = (Seat) other; return row == s.row && number == s.number; }
@Override public int hashCode() { return 31 * row + number; }}Mit demselben Code in main wie in der ersten Karte:
Map<Seat, String> bookings = new HashMap<>();bookings.put(new Seat(5, 12), "Maria");
System.out.println(bookings.get(new Seat(5, 12)));System.out.println(bookings.size());bookings.put(new Seat(5, 12), "Jonas");System.out.println(bookings.size());Maria11Die Regeln, die Java erwartet:
- Wenn
a.equals(b), danna.hashCode() == b.hashCode(). Immer. - Wenn die Hashcodes gleich sind, können die Objekte verschieden sein. Das ist eine Kollision.
- Wenn du
equalsüberschreibst, überschreib auchhashCode, mit denselben Feldern.
Genauer, für Neugierige
- Warum 31? Es ist eine ungerade Primzahl, und sie mischt gut. Die Multiplikation mit 31 macht den Hash von der Reihenfolge abhängig: Platz 5/12 und Platz 12/5 haben verschiedene Hashcodes. Mit
row + numberwürden beide im selben Bucket landen. - Statt der Formel kannst du
Objects.hashausjava.util.Objectsschreiben. Es berechnet dasselbe aus den Feldern, die du ihm gibst:
@Overridepublic int hashCode() { return Objects.hash(row, number);}equalsbekommt einObject, deshalb prüft es zuerst mitinstanceof, ob das andere überhaupt einSeatist, und wandelt es dann um. Das ist der Downcast aus dem Modul „Vererbung und Polymorphie“.
Probier es aus
Finde den Fehler
HashSet. Zwei Produkte mit demselben Code sind dasselbe Produkt, auch wenn sich der Preis geändert hat. Aber contains mit dem neuen Preis gibt false zurück:Set<Product> catalog = new HashSet<>();catalog.add(new Product("A-100", 25.0));// Der Preis ist gesunken, aber es ist dasselbe Produkt.System.out.println(catalog.contains(new Product("A-100", 19.9)));falseIn welcher Zeile der Klasse steckt der Fehler?
Wähle die Zeile mit dem Fehler.
equals vergleicht nur den Code, aber hashCode nutzt auch den Preis. Die beiden Produkte sind laut equals gleich, haben aber verschiedene Hashcodes. Das bricht den Vertrag, also sucht das HashSet in einem anderen Bucket und vergleicht sie gar nicht erst. hashCode muss dieselben Felder nutzen wie equals: return code.hashCode();. Dann gibt contains true zurück.Die Idee
Die Buckets füllen sich
Bei vielen Paaren werden die Buckets lang, und die Suche wird langsamer. Deshalb wächst eine HashMap:
- Am Anfang gibt es 16 Buckets.
- Gibt es mehr Paare als 75 % der Anzahl der Buckets, legt sie doppelt so viele Buckets an und verteilt alle Schlüssel neu. Die Bucket-Nummer hängt von der Anzahl der Buckets ab, deshalb wird jeder Schlüssel neu berechnet.
- Deshalb sind
putundgetim Durchschnitt schnell, aber ab und zu dauert einputlänger.
Es gibt auch eine Falle. Ein Schlüssel darf sich nicht ändern, während er in einer Map oder einem Set liegt:
Seat seat = new Seat(5, 12);Set<Seat> sold = new HashSet<>();sold.add(seat);
seat.number = 13;System.out.println(sold.contains(seat));System.out.println(sold.contains(new Seat(5, 12)));System.out.println(sold.size());falsefalse1contains(seat) sucht im Bucket für 5/13 und findet ihn nicht. contains(new Seat(5, 12)) sucht im richtigen Bucket, aber das Objekt dort ist nicht mehr gleich 5/12. Das Element ist im Set (die Größe ist 1), aber es kann nicht gefunden werden. Deshalb ist es eine gute Idee, dass die Felder eines Schlüssels final sind.Genauer, für Neugierige
- Diese 75 % heißen Ladefaktor (load factor), und du kannst ihn im Konstruktor setzen:
new HashMap<>(64, 0.75f). Weißt du, wie viele Schlüssel es geben wird, gib die Anfangszahl der Buckets an, dann muss die Map unterwegs nicht wachsen. - Ab Java 8 wird ein Bucket mit mehr als 8 Paaren (
TREEIFY_THRESHOLD) von einer Liste zu einem balancierten Baum, wenn es mindestens 64 Buckets gibt. Bleiben nach einem Neuverteilen 6 oder weniger Paare in so einem Bucket (UNTREEIFY_THRESHOLD), wird der Baum wieder zur Liste. So braucht eine Suche bei vielen Kollisionen in einem Bucket mit n Paaren etwa log n Vergleiche statt n.
Übung
Eigene Hashcodes
Löse die Aufgaben in deinem Editor.
1. Ein Hashcode für eine Person. Eine Klasse Person mit Vorname, Nachname und Alter. Überschreib equals und hashCode. Nimm für hashCode das Rezept: Beginne bei 17, und für jedes Feld result = 31 * result + der Hash des Feldes. Für ein int-Feld ist der Hash die Zahl selbst. Leg Jonas Weber mit 30 zweimal an und einmal mit 31. Gib die Hashcodes aus und wie viele verschiedene Personen in einem HashSet bleiben.
-1469209977-1469209977-1469209976Verschiedene Personen: 22. Der Roboter im Lager. Der Roboter startet in Zelle (0, 0) und macht die Schritte NNEESWWN: N ist nach oben (y + 1), S nach unten, E nach rechts (x + 1), W nach links. Wie viele verschiedene Zellen hat er besucht, die Startzelle mitgezählt? Halte sie in einem HashSet<Point>. Mit dieser Klasse sagt das Programm 9, es sollte aber 7 sagen. Repariere es:
class Point { int x; int y;
Point(int x, int y) { this.x = x; this.y = y; }}Schritte: 8Verschiedene Zellen: 73. Ein Hash einer Datei. Eine Methode hashFileContent(String filePath) liest die Datei und gibt den Hashcode ihres Inhalts als hexadezimalen Text zurück: Integer.toHexString(content.hashCode()). Schreib drei Berichte: a.txt und b.txt mit „Bericht für Oktober: 1250 €“ und c.txt mit „Bericht für Oktober: 1260 €“. Vergleiche ihre Hashes. Verschiedene Hashes heißen, dass die Dateien sicher verschieden sind. Gleiche Hashes heißen „fast sicher gleich“: Bei einem int ist eine Kollision möglich, deshalb nimmt man für eine echte Prüfung SHA-256.
a.txt: ad5bbcd3b.txt: ad5bbcd3c.txt: ad69d454a und b: gleicha und c: verschiedenWeitere Aufgaben
- Eine eigene Hashtabelle. Eine Fortsetzung der Benutzertabelle aus der letzten Lektion, diesmal ohne
HashMap. Leg ein Array aus 8 Buckets an, jeder eineArrayListaus Paaren von Benutzername und E-Mail. Die Hashfunktion ist die Summe der Zeichencodes im Namen. Schreibadd,getund eine Methode, die ausgibt, wie viele Paare in jedem Bucket sind. - Eine ID aus einer E-Mail.
generateUserId(String email)gibt"U" + Integer.toHexString(email.toLowerCase().hashCode())zurück. Ist die ID fürJonas@Example.comundjonas@example.comdieselbe? Können zwei verschiedene E-Mails dieselbe ID bekommen? Wie würdest du es herausfinden? - Kollisionen zählen. Berechne den Bucket der Schlüssel
user1…user1000bei 16, 64 und 1024 Buckets und zähle, wie viele Schlüssel in einem Bucket landen, in dem schon ein Schlüssel ist. - Der Schuhkarton. Eine Klasse
Boxmit Breite, Höhe und Tiefe. Zwei Kartons sind gleich, wenn ihre Maße gleich sind, in derselben Reihenfolge. SchreibequalsundhashCodemitObjects.hashund prüfe die Kartons 30×20×10 und 10×20×30 in einemHashSet.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Dateien · Listen · Stack, Queue und verkettete Liste · HashMap und HashSet
- Geht weiter in: record, switch und Muster · Hashtabelle · Passwörter · Abhängigkeiten: fremder Code, den dein Code ausführt
- Aufgaben zum Thema: Collections