Eine Einführung in die Java.util.Hashtable-Klasse

1. Überblick

  • Hashtable ist die älteste Implementierung einer Hash-Tabellendatenstruktur in Java. die in JDK 1.2 eingeführt wurde.

Beide Klassen bieten ähnliche Funktionen, es gibt jedoch auch kleine Unterschiede.

2. Wann ist Hashtable zu verwenden?

Nehmen wir an, wir haben ein Wörterbuch, in dem jedes Wort seine Definition hat.

Außerdem müssen wir Wörter schnell aus dem Wörterbuch holen, einfügen und entfernen.

Daher ist Hashtable (oder HashMap ) sinnvoll. Wörter sind die Schlüssel in der Hashtable , da sie eindeutig sein sollen. Definitionen dagegen sind die Werte.

3. Anwendungsbeispiel

Fahren wir mit dem Wörterbuch-Beispiel fort. Wir werden Word als Schlüssel modellieren:

public class Word {
    private String name;

    public Word(String name) {
        this.name = name;
    }

   //...
}

Angenommen, die Werte sind Strings . Jetzt können wir eine Hashtable erstellen:

Hashtable<Word, String> table = new Hashtable<>();

Zuerst fügen wir einen Eintrag hinzu:

Word word = new Word("cat");
table.put(word, "an animal");

Um auch einen Eintrag zu bekommen:

String definition = table.get(word);

Zum Schluss entfernen wir einen Eintrag:

definition = table.remove(word);

Es gibt viele weitere Methoden in der Klasse, von denen wir später einige beschreiben werden.

Lassen Sie uns zunächst über einige Anforderungen an das Schlüsselobjekt sprechen.

4. Die Bedeutung von hashCode ()

  • Um als Schlüssel in einer Hashtable verwendet zu werden, darf das Objekt den Link nicht verletzen:/java-hashcode[ hashCode () contract.]** Kurz gesagt, gleiche Objekte müssen denselben Code zurückgeben. Um zu verstehen, warum das so ist, wird die Hash-Tabelle organisiert.

Hashtable verwendet ein Array. Jede Position im Array ist ein "Bucket", der entweder Null sein kann oder ein oder mehrere Schlüssel-Wert-Paare enthalten kann. Der Index jedes Paares wird berechnet.

Aber warum nicht Elemente sequentiell speichern und am Ende des Arrays neue Elemente hinzufügen?

Der springende Punkt ist, dass das Finden eines Elements nach Index viel schneller ist, als die Elemente mit dem Vergleich sequenziell zu durchlaufen. Daher benötigen wir eine Funktion, die Schlüssel zu Indizes bildet.

4.1. Direkte Adressentabelle

Das einfachste Beispiel für eine solche Zuordnung ist die Direktadressentabelle. Hier werden Schlüssel als Indizes verwendet:

index(k)=k,
where k is a key

Schlüssel sind eindeutig, dh jeder Bereich enthält ein Schlüssel-Wert-Paar. Diese Technik eignet sich gut für ganzzahlige Schlüssel, wenn der mögliche Bereich davon recht klein ist.

Aber wir haben hier zwei Probleme:

  • Erstens sind unsere Schlüssel keine Ganzzahlen, sondern Word -Objekte

  • Zweitens: Wenn es sich um Ganzzahlen handelt, kann niemand garantieren, dass sie klein sind.

Stellen Sie sich vor, die Schlüssel sind 1, 2 und 1000000. Wir haben ein großes Array mit der Größe 1000000 mit nur drei Elementen, und der Rest ist ein verschwendeter Platz

Die hashCode () Methode löst das erste Problem.

Die Logik für die Datenmanipulation in der Hashtable löst das zweite Problem.

Lassen Sie uns dies ausführlich besprechen.

4.2. hashCode () Methode

Jedes Java-Objekt erbt die hashCode () -Methode, die einen int -Wert zurückgibt. Dieser Wert wird aus der internen Speicheradresse des Objekts berechnet. Standardmäßig gibt hashCode () eindeutige Ganzzahlen für verschiedene Objekte zurück.

Daher kann jedes Schlüsselobjekt mit hashCode () ** in eine Ganzzahl konvertiert werden.

Diese ganze Zahl kann jedoch groß sein.

4.3. Reichweite reduzieren

Die Methoden get () , put () und remove () enthalten den Code, mit dem das zweite Problem gelöst wird - der Bereich möglicher Ganzzahlen wird reduziert.

Die Formel berechnet einen Index für den Schlüssel:

int index = (hash & 0x7FFFFFFF) % tab.length;

Dabei ist tab.length die Arraygröße und hash eine Zahl, die von der hashCode () -Methode des Schlüssels zurückgegeben wird.

Wie wir sehen, ist index eine Erinnerung an die Division hash durch die Arraygröße . Beachten Sie, dass gleiche Hashcodes denselben Index erzeugen.

4.4. Kollisionen

Darüber hinaus können auch verschiedene Hash-Codes denselben Index erzeugen. Wir bezeichnen das als Kollision. Um Kollisionen aufzulösen, speichert Hashtable eine LinkedList von Schlüsselwertpaaren.

Diese Datenstruktur wird Hash-Tabelle mit Verkettung genannt.

4.5. Ladefaktor

Es ist leicht zu vermuten, dass Kollisionen den Betrieb mit Elementen verlangsamen.

Um einen Eintrag zu erhalten, reicht es nicht aus, seinen Index zu kennen, aber wir müssen die Liste durchgehen und einen Vergleich mit jedem Eintrag durchführen.

Daher ist es wichtig, die Anzahl der Kollisionen zu reduzieren. Je größer ein Array ist, desto geringer ist die Wahrscheinlichkeit einer Kollision. Der Auslastungsfaktor bestimmt die Balance zwischen der Arraygröße und der Leistung. Standardmäßig ist der Wert 0,75, dh die Arraygröße verdoppelt sich, wenn 75% der Buckets nicht leer sind. Diese Operation wird von der Methode rehash () ausgeführt.

Aber zurück zu den Schlüsseln.

4.6. Überschreiben von equals () und hashCode ()

Wenn wir einen Eintrag in eine Hashtable einfügen und daraus herausholen, erwarten wir, dass der Wert nicht nur mit derselben Instanz des Schlüssels, sondern auch mit einem gleichen Schlüssel abgerufen werden kann:

Word word = new Word("cat");
table.put(word, "an animal");
String extracted = table.get(new Word("cat"));

Um die Gleichheitsregeln festzulegen, überschreiben wir die Methode equals () des Schlüssels:

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Word))
        return false;

    Word word = (Word) o;
    return word.getName().equals(this.name);
}

Wenn wir hashCode () jedoch nicht überschreiben, wenn Sie equals () überschreiben, können zwei gleiche Schlüssel in den verschiedenen Buckets landen, da Hashtable den Index des Schlüssels anhand seines Hash-Codes berechnet.

Schauen wir uns das obige Beispiel genauer an. Was passiert, wenn wir hashCode () nicht überschreiben?

  • Zwei Instanzen von Word sind hier involviert - die erste ist für das Putten

der Eintrag und der zweite ist für den Erhalt des Eintrags. Obwohl diese Instanzen sind gleich, ihre hashCode () -Methode gibt unterschiedliche Zahlen zurück ** Der Index für jeden Schlüssel wird anhand der Formel aus Abschnitt 4.3 berechnet.

Entsprechend dieser Formel können verschiedene Hash-Codes unterschiedliche produzieren Indizes ** Dies bedeutet, dass wir den Eintrag in einen Eimer legen und dann versuchen zu bekommen

es aus dem anderen Eimer heraus. Diese Logik bricht Hashtable

  • Equal-Schlüssel müssen gleiche Hash-Codes zurückgeben, deshalb überschreiben wir die hashCode () -Methode: **

public int hashCode() {
    return name.hashCode();
}

Beachten Sie, dass es auch empfohlen wird, dass ungleiche Schlüssel unterschiedliche Hash-Codes zurückgeben , da sie sonst im selben Bucket landen. Dies wird die Leistung beeinträchtigen und somit einige der Vorteile einer Hashtable verlieren.

Beachten Sie auch, dass uns die Schlüssel von String , Integer , Long oder einem anderen Wrapper-Typ egal sind. Beide Methoden equal () und hashCode () werden in Wrapper-Klassen bereits überschrieben.

5. Hashtables iterieren

Es gibt mehrere Möglichkeiten, ____Hashtables zu iterieren. In diesem Abschnitt sollten Sie gut über sie sprechen und einige der Implikationen erläutern.

5.1. Schnell fehlschlagen: Iteration

Fail-Fast-Iteration bedeutet, dass eine Hashtable geändert wird, nachdem ihr _Iterator erstellt wurde, die ConcurrentModificationException_ ausgelöst wird. Lassen Sie uns das demonstrieren.

Zuerst erstellen wir eine Hashtable und fügen Einträge hinzu:

Hashtable<Word, String> table = new Hashtable<Word, String>();
table.put(new Word("cat"), "an animal");
table.put(new Word("dog"), "another animal");

Zweitens erstellen wir einen Iterator :

Iterator<Word> it = table.keySet().iterator();

Und drittens ändern wir die Tabelle:

table.remove(new Word("dog"));

Wenn wir jetzt versuchen, die Tabelle zu durchlaufen, erhalten wir eine ConcurrentModificationException

while (it.hasNext()) {
    Word key = it.next();
}
java.util.ConcurrentModificationException
    at java.util.Hashtable$Enumerator.next(Hashtable.java:1378)

ConcurrentModificationException hilft dabei, Fehler zu finden und somit unvorhersehbares Verhalten zu vermeiden, wenn beispielsweise ein Thread die Tabelle durchläuft und ein anderer versucht, sie gleichzeitig zu ändern.

5.2. Nicht schnell scheitern: Enumeration

Enumeration in einer Hashtable ist nicht ausfallsicher. Schauen wir uns ein Beispiel an.

Zuerst erstellen wir eine Hashtable und fügen Einträge hinzu:

Hashtable<Word, String> table = new Hashtable<Word, String>();
table.put(new Word("1"), "one");
table.put(new Word("2"), "two");

Zweitens erstellen wir eine Enumeration :

Enumeration<Word> enumKey = table.keys();

Drittens ändern wir die Tabelle:

table.remove(new Word("1"));

Wenn wir nun die Tabelle durchlaufen, wird keine Ausnahme ausgelöst:

while (enumKey.hasMoreElements()) {
    Word key = enumKey.nextElement();
}

5.3. Unvorhersehbare Iterationsreihenfolge

Beachten Sie außerdem, dass die Iterationsreihenfolge in einer Hashtable nicht vorhersagbar ist und nicht der Reihenfolge entspricht, in der die Einträge hinzugefügt wurden.

Dies ist verständlich, da jeder Index anhand des Hashcodes des Schlüssels berechnet wird. Darüber hinaus findet von Zeit zu Zeit ein Nachwaschen statt, wobei die Reihenfolge der Datenstruktur geändert wird.

Fügen wir also einige Einträge hinzu und überprüfen Sie die Ausgabe:

Hashtable<Word, String> table = new Hashtable<Word, String>();
    table.put(new Word("1"), "one");
    table.put(new Word("2"), "two");
   //...
    table.put(new Word("8"), "eight");

    Iterator<Map.Entry<Word, String>> it = table.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry<Word, String> entry = it.next();
       //...
    }
}
five
four
three
two
one
eight
seven

6. Hashtable vs. HashMap

Hashtable und HashMap bieten sehr ähnliche Funktionen.

Beide bieten:

  • Fail-Fast-Iteration

  • Unvorhersehbare Iterationsreihenfolge

Es gibt aber auch einige Unterschiede:

  • HashMap stellt keine Enumeration bereit, während Hashtable bereitstellt

nicht ausfallsicher Enumeration ** Hashtable erlaubt keine null -Schlüssel und null -Werte, während

HashMap erlaubt einen null -Schlüssel und eine beliebige Anzahl von null -Werten ** Hashtable s Methoden werden synchronisiert, während HashMaps s Methoden sind

nicht

7. Hashtable API in Java 8

Java 8 hat neue Methoden eingeführt, mit denen unser Code sauberer wird. Insbesondere können wir einige if -Blöcke loswerden. Lassen Sie uns das demonstrieren.

7.1. getOrDefault ()

Nehmen wir an, wir müssen die Definition des Wortes "dog _" _ bekommen und es der Variablen zuweisen, wenn sie auf dem Tisch liegt. Anderenfalls weisen Sie der Variablen "nicht gefunden" zu.

Vor Java 8:

Word key = new Word("dog");
String definition;

if (table.containsKey(key)) {
     definition = table.get(key);
} else {
     definition = "not found";
}

Nach Java 8:

definition = table.getOrDefault(key, "not found");

7.2. putIfAbsent ()

Nehmen wir an, wir müssen ein Wort „cat _“ _ nur einfügen, wenn es noch nicht im Wörterbuch enthalten ist

Vor Java 8:

if (!table.containsKey(new Word("cat"))) {
    table.put(new Word("cat"), definition);
}

Nach Java 8:

table.putIfAbsent(new Word("cat"), definition);

7.3. boolean remove ()

Nehmen wir an, wir müssen das Wort „Katze“ entfernen, aber nur, wenn „Tier“ definiert ist.

Vor Java 8:

if (table.get(new Word("cat")).equals("an animal")) {
    table.remove(new Word("cat"));
}

Nach Java 8:

boolean result = table.remove(new Word("cat"), "an animal");

Während die alte Methode remove () den Wert zurückgibt, gibt die neue Methode boolean zurück.

7.4. ersetzen()

Nehmen wir an, wir müssen die Definition von "Katze" ersetzen, aber nur wenn ihre alte Definition "ein kleines domestiziertes Fleisch fressendes Säugetier" ist

Vor Java 8:

if (table.containsKey(new Word("cat"))
    && table.get(new Word("cat")).equals("a small domesticated carnivorous mammal")) {
    table.put(new Word("cat"), definition);
}

Nach Java 8:

table.replace(new Word("cat"), "a small domesticated carnivorous mammal", definition);

7.5. computeIfAbsent ()

Diese Methode ähnelt putIfabsent () . PutIfabsent () nimmt den Wert direkt und computeIfAbsent () eine Zuordnungsfunktion. Es berechnet den Wert erst, nachdem er den Schlüssel überprüft hat. Dies ist effizienter, insbesondere wenn der Wert schwer zu erhalten ist.

table.computeIfAbsent(new Word("cat"), key -> "an animal");

Daher entspricht die obige Zeile:

if (!table.containsKey(cat)) {
    String definition = "an animal";//note that calculations take place inside if block
    table.put(new Word("cat"), definition);
}

7.6. computeIfPresent ()

Diese Methode ähnelt der Methode replace () . Replace () übernimmt den Wert jedoch direkt und computeIfPresent () eine Mapping-Funktion. Es berechnet den Wert innerhalb des if -Blocks und ist daher effizienter.

Nehmen wir an, wir müssen die Definition ändern:

table.computeIfPresent(cat, (key, value) -> key.getName() + " - " + value);

Daher entspricht die obige Zeile:

if (table.containsKey(cat)) {
    String concatination=cat.getName() + " - " + table.get(cat);
    table.put(cat, concatination);
}

7.7. berechnen()

Jetzt lösen wir eine andere Aufgabe. Angenommen, wir haben ein Array von String , bei dem die Elemente nicht eindeutig sind. Lassen Sie uns auch berechnen, wie viele Vorkommen eines Strings wir im Array erhalten können. Hier ist das Array:

String[]animals = { "cat", "dog", "dog", "cat", "bird", "mouse", "mouse" };

Außerdem möchten wir eine Hashtable erstellen, die ein Tier als Schlüssel und die Anzahl seiner Vorkommen als Wert enthält.

Hier ist eine Lösung:

Hashtable<String, Integer> table = new Hashtable<String, Integer>();

for (String animal : animals) {
    table.compute(animal,
        (key, value) -> (value == null ? 1 : value + 1));
}

Schließlich stellen wir sicher, dass der Tisch zwei Katzen, zwei Hunde, einen Vogel und zwei Mäuse enthält:

assertThat(table.values(), hasItems(2, 2, 2, 1));

7.8. verschmelzen()

Es gibt einen anderen Weg, um die obige Aufgabe zu lösen:

for (String animal : animals) {
    table.merge(animal, 1, (oldValue, value) -> (oldValue + value));
}

Das zweite Argument, 1 , ist der Wert, der dem Schlüssel zugeordnet wird, wenn der Schlüssel noch nicht in der Tabelle vorhanden ist. Wenn sich der Schlüssel bereits in der Tabelle befindet, berechnen wir ihn als oldValue 1 .

7.9. für jeden()

Dies ist eine neue Möglichkeit, die Einträge zu durchlaufen. Lassen Sie uns alle Einträge ausdrucken:

table.forEach((k, v) -> System.out.println(k.getName() + " - " + v)

7.10. alles ersetzen()

Zusätzlich können wir alle Werte ohne Wiederholung ersetzen:

table.replaceAll((k, v) -> k.getName() + " - " + v);

8. Fazit

In diesem Artikel haben wir den Zweck der Hashtabellenstruktur beschrieben und gezeigt, wie die Struktur einer Direktadressentabelle kompliziert wird, um sie zu erhalten.

Außerdem haben wir beschrieben, was Kollisionen sind und welchen Lastfaktor eine Hashtable hat. Außerdem haben wir gelernt, warum equals () und hashCode () __ für Schlüsselobjekte überschrieben werden sollen.

Schließlich haben wir über die Eigenschaften von Hashtable und die Java 8-spezifische API gesprochen.

Der vollständige Quellcode ist wie üblich verfügbar: https://github.com/eugenp/tutorials/tree/master/core-java-collections