webentwicklung-frage-antwort-db.com.de

Falsche Idee, String-Schlüssel in HashMap zu verwenden?

Ich verstehe, dass die String-Klasse ' hashCode () -Methode nicht garantiert ist, um eindeutige Hash-Codes für bestimmte Strings zu generieren. Ich sehe eine Menge Verwendung von String-Schlüsseln in HashMap-s (mit der Standardmethode String hashCode ()). Ein Großteil dieser Verwendung kann zu erheblichen Anwendungsproblemen führen, wenn eine Map put einen HashMap-Eintrag ersetzt, der zuvor mit einem wirklich eindeutigen String-Schlüssel auf der Map platziert wurde.

Was sind die Chancen, dass Sie in dem Szenario, in dem String.hashCode () denselben Wert für bestimmte Zeichenfolgen zurückgibt, ausgeführt werden? Wie umgehen Entwickler dieses Problem, wenn der Schlüssel ein String ist?

66
Marcus Leon

Entwickler müssen das Problem der Hash-Kollisionen in HashMap nicht umgehen, um die Programmkorrektheit zu erreichen.

Hier sind einige wichtige Dinge zu verstehen:

  1. Kollisionen sind ein inhärentes Merkmal von Hashing und müssen es auch sein. Die Anzahl der möglichen Werte (Strings in Ihrem Fall, gilt aber auch für andere Typen) ist erheblich größer als der Bereich der Ganzzahlen.
  2. Jede Verwendung von Hashing hat eine Möglichkeit, mit Kollisionen umzugehen, und die Java Collections (einschließlich HashMap) sind keine Ausnahme.
  3. Hashing ist nicht an Gleichheitstests beteiligt. Es ist wahr, dass gleiche Objekte gleiche Hashcodes haben müssen, aber das Gegenteil ist nicht der Fall: Viele Werte haben denselben Hashcode. Versuchen Sie also nicht, einen Hashcode-Vergleich als Ersatz für Gleichheit zu verwenden. Sammlungen nicht. Sie verwenden Hashing, um eine Untersammlung (in der Java Collections-Welt) als Bucket bezeichnet) auszuwählen, verwenden jedoch .equals (), um tatsächlich die Gleichheit zu überprüfen.
  4. Sie müssen sich nicht nur keine Gedanken über Kollisionen machen, die zu falschen Ergebnissen in einer Sammlung führen, sondern für die meisten Anwendungen müssen Sie sich * normalerweise * auch keine Gedanken über die Leistung machen - Java hashed Collections do a ziemlich gute Arbeit bei der Verwaltung von Hashcodes.
  5. Besser noch, für den Fall, dass Sie nach (Strings as keys) gefragt haben, müssen Sie sich nicht einmal um die Hashcodes selbst kümmern, da die String-Klasse von Java einen ziemlich guten Hashcode generiert. Tun Sie so die meisten der gelieferten Klassen Java.

Noch ein paar Details, wenn Sie es wollen:

Die Funktionsweise von Hashing (insbesondere bei Hashing-Sammlungen wie der HashMap von Java, nach der Sie gefragt haben) ist folgende:

  • Die HashMap speichert die von Ihnen angegebenen Werte in einer Sammlung von Untersammlungen, die als Buckets bezeichnet werden. Diese sind tatsächlich als verknüpfte Listen implementiert. Es gibt eine begrenzte Anzahl von diesen: iirc, 16, um standardmäßig zu starten, und die Anzahl erhöht sich, wenn Sie mehr Elemente in die Karte einfügen. Es sollte immer mehr Eimer als Werte geben. Um ein Beispiel zu geben: Wenn Sie unter Verwendung der Standardeinstellungen 100 Einträge zu einer HashMap hinzufügen, gibt es 256 Buckets.

  • Jeder Wert, der als Schlüssel in einer Karte verwendet werden kann, muss in der Lage sein, einen ganzzahligen Wert, den so genannten Hashcode, zu generieren.

  • Die HashMap verwendet diesen Hashcode, um einen Bucket auszuwählen. Letztendlich bedeutet dies, dass der ganzzahlige Wert modulo für die Anzahl der Buckets verwendet wird. Zuvor verfügt Javas HashMap jedoch über eine interne Methode (hash()), mit der der Hashcode optimiert wird, um einige bekannte Quellen von zu reduzieren klumpen.

  • Bei der Suche nach einem Wert wählt die HashMap den Bucket aus und sucht dann nach dem einzelnen Element durch eine lineare Suche in der verknüpften Liste mit .equals().

Also: Sie müssen Kollisionen nicht umgehen, um die Korrektheit zu gewährleisten, und Sie müssen sich normalerweise keine Sorgen um die Leistung machen, und wenn Sie native Java Klassen (wie String) verwenden. müssen Sie sich auch nicht um die Generierung der Hashcode-Werte kümmern.

In dem Fall, dass Sie Ihre eigene Hashcode-Methode schreiben müssen (was bedeutet, dass Sie eine Klasse mit einem zusammengesetzten Wert geschrieben haben, wie z. B. ein Vorname/Nachname-Paar), wird die Sache etwas komplizierter. Es ist durchaus möglich, hier etwas falsch zu machen, aber es ist keine Raketenwissenschaft. Beachten Sie zunächst Folgendes: Das einzige, was Sie tun müssen, um die Richtigkeit sicherzustellen, ist sicherzustellen, dass gleiche Objekte gleiche Hashcodes ergeben. Wenn Sie also eine hashcode () -Methode für Ihre Klasse schreiben, müssen Sie auch eine equals () -Methode schreiben und die gleichen Werte in jeder prüfen.

Es ist möglich, eine Hashcode () -Methode zu schreiben, die zwar schlecht, aber korrekt ist, mit der ich meine, dass sie die Bedingung "Gleiche Objekte müssen gleiche Hashcodes ergeben" erfüllen würde, aber dennoch sehr schlecht abschneidet, wenn sie viele Kollisionen aufweist.

Der kanonisch entartete schlimmste Fall wäre das Schreiben einer Methode, die für alle Fälle einfach einen konstanten Wert (z. B. 3) zurückgibt. Dies würde bedeuten, dass jeder Wert in denselben Bucket gehasht wird.

Es würde immer noch funktionieren, aber die Leistung würde sich auf die einer verknüpften Liste verschlechtern.

Offensichtlich werden Sie keine so schreckliche hashcode () -Methode schreiben. Wenn Sie eine anständige IDE verwenden, kann sie eine für Sie generieren. Da StackOverflow Code liebt, ist hier der Code für die oben genannte Vor-/Nachname-Klasse.


public class SimpleName {
    private String firstName;
    private String lastName;
    public SimpleName(String firstName, String lastName) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((firstName == null) ? 0 : firstName.hashCode());
        result = prime * result
                + ((lastName == null) ? 0 : lastName.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SimpleName other = (SimpleName) obj;
        if (firstName == null) {
            if (other.firstName != null)
                return false;
        } else if (!firstName.equals(other.firstName))
            return false;
        if (lastName == null) {
            if (other.lastName != null)
                return false;
        } else if (!lastName.equals(other.lastName))
            return false;
        return true;
    }
}

113
CPerkins

Ich vermute stark, dass die HashMap.put Methode ermittelt nicht, ob der Schlüssel derselbe ist, indem sie nur String.hashCode .

Es wird definitiv eine Chance für eine Hash-Kollision geben , daher würde man erwarten, dass die String.equals Methode wird auch aufgerufen, um sicherzustellen, dass die String wirklich gleich sind, wenn es tatsächlich einen Fall gibt, in dem die beiden String s haben denselben Wert wie hashCode.

Daher wird der neue Schlüssel String nur dann als derselbe Schlüssel String beurteilt, der sich bereits in HashMap befindet, wenn der von zurückgegebene Wert - hashCode ist gleich, und die Methode equals gibt true zurück.

Außerdem gilt dieser Gedanke auch für andere Klassen als String, da die Object Klasse selbst bereits die hashCode und equals Methoden.

Bearbeiten

Um die Frage zu beantworten, nein, es wäre keine schlechte Idee, ein String als Schlüssel für ein HashMap zu verwenden.

4
coobird

Dies ist kein Problem, es ist nur, wie Hashtables funktionieren. Es ist nachweislich unmöglich, eindeutige Hashcodes für alle eindeutigen Zeichenfolgen zu haben, da es weit mehr eindeutige Zeichenfolgen als ganze Zahlen gibt.

Wie andere geschrieben haben, werden Hash-Kollisionen mit der equals () -Methode aufgelöst. Das einzige Problem, das dies verursachen kann, ist die Degeneration der Hash-Tabelle, die zu einer schlechten Leistung führt. Deshalb hat Javas HashMap einen Ladefaktor , ein Verhältnis zwischen Buckets und eingefügten Elementen, bei dessen Überschreitung die Tabelle mit der doppelten Anzahl von Buckets erneut aufgewärmt wird.

Dies funktioniert im Allgemeinen sehr gut, aber nur, wenn die Hash-Funktion gut ist, d. H. Nicht mehr als die statistisch erwartete Anzahl von Kollisionen für Ihren bestimmten Eingabesatz ergibt. String.hashCode() ist in dieser Hinsicht gut, aber das war nicht immer so. Angeblich , vor Java 1.2 enthielt es nur jedes n-te Zeichen. Dies war schneller, verursachte aber vorhersehbare Kollisionen für alle Strings, die jedes n-te Zeichen gemeinsam hatten - sehr Schlimm, wenn Sie nicht genug Glück haben, um solche regelmäßigen Eingaben zu machen, oder wenn jemand eine DOS-Attacke auf Ihre App ausführen möchte.

4

Ich leite Sie auf die Antwort hier . Es ist zwar keine schlecht Idee, Strings zu verwenden (@CPerkins erklärt, warum, perfekt), aber das Speichern der Werte in einer Hashmap mit Ganzzahlschlüsseln ist sinnvoll besser, da es in der Regel schneller (wenn auch unbemerkt) ist und eine geringere Wahrscheinlichkeit (eigentlich keine Chance) für Kollisionen hat.

Sehen Sie sich diese Kollisionstabelle mit jeweils 216553 Schlüsseln an (gestohlen von post , für unsere Diskussion neu formatiert).

Hash           Lowercase      Random UUID  Numbers 
=============  =============  ===========  ==============
Murmur            145 ns      259 ns          92 ns
                    6 collis    5 collis       0 collis
FNV-1a            152 ns      504 ns          86 ns
                    4 collis    4 collis       0 collis
FNV-1             184 ns      730 ns          92 ns
                    1 collis    5 collis       0 collis*
DBJ2a             158 ns      443 ns          91 ns
                    5 collis    6 collis       0 collis***
DJB2              156 ns      437 ns          93 ns
                    7 collis    6 collis       0 collis***
SDBM              148 ns      484 ns          90 ns
                    4 collis    6 collis       0 collis**
CRC32             250 ns      946 ns         130 ns
                    2 collis    0 collis       0 collis

Avg Time per key    0.8ps       2.5ps         0.44ps
Collisions (%)      0.002%      0.002%         0%

Natürlich ist die Anzahl der Ganzzahlen auf 2 ^ 32 begrenzt, da es keine Begrenzung für die Anzahl der Zeichenfolgen gibt (und es keine theoretische Begrenzung für die Anzahl der Schlüssel gibt, die in einem HashMap gespeichert werden können). . Wenn Sie ein long (oder sogar ein float) verwenden, sind Kollisionen unvermeidlich und daher nicht "besser" als eine Zeichenfolge. Trotz Hash-Kollisionen erhalten put() und get() immer das richtige Schlüssel-Wert-Paar (siehe Bearbeitung unten).

Am Ende ist es wirklich egal, also verwenden Sie, was bequemer ist. Aber wenn die Bequemlichkeit keinen Unterschied macht und Sie nicht mehr als 2 ^ 32 Einträge haben möchten, empfehle ich, ints als Schlüssel zu verwenden.


[~ # ~] edit [~ # ~]

Obwohl dies definitiv zutrifft, verwenden Sie NIEMALS "StringKey" .hashCode (), um aus Leistungsgründen einen Schlüssel anstelle des ursprünglichen Schlüssels String zu generieren. 2 verschiedene Zeichenfolgen können denselben HashCode haben, wodurch das Überschreiben Ihres put() verursacht wird. Methode. Javas Implementierung von HashMap ist intelligent genug, um Zeichenfolgen (eigentlich jede Art von Schlüssel) mit demselben Hashcode automatisch zu verarbeiten. Es ist daher ratsam, Java) diese Dinge für Sie erledigen zu lassen .

4
dberm22

Sie sprechen von Hash-Kollisionen. Hash-Kollisionen sind ein Problem, unabhängig davon, welcher Typ hashCode-fähig ist. Alle Klassen, die hashCode verwenden (z. B. HashMap), verarbeiten Hash-Kollisionen einwandfrei. Beispielsweise kann HashMap mehrere Objekte pro Bucket speichern.

Machen Sie sich keine Sorgen, es sei denn, Sie rufen selbst hashCode auf. Hash-Kollisionen sind zwar selten, brechen aber nichts.

2
Keith Randall