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?
Entwickler müssen das Problem der Hash-Kollisionen in HashMap nicht umgehen, um die Programmkorrektheit zu erreichen.
Hier sind einige wichtige Dinge zu verstehen:
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;
}
}
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.
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.
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 .
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.