W3docs

Java LinkedHashSet

LinkedHashSet in Java nutzen, um die Einfügereihenfolge zu bewahren und gleichzeitig die schnellen Operationen von HashSet zu behalten.

LinkedHashSet<E> ist HashSet<E> mit einem zusätzlichen Versprechen: Beim Iterieren erhält man die Elemente in der Reihenfolge, in der sie zuerst eingefügt wurden. Die Hash-Tabellen-Mechanik ist identisch — gleiche Buckets, gleicher Lastfaktor, gleiche O(1)-Operationen add, remove, contains — aber jeder Eintrag trägt zwei zusätzliche Zeiger (before, after), die die Einträge beim Hinzufügen zu einer doppelt verketteten Liste verbinden. Die Iteration folgt dieser Liste, nicht dem Bucket-Array.

Wenn man Hash-Set-Performance und eine deterministische, vorhersagbare Iterationsreihenfolge benötigt, ist LinkedHashSet die Antwort. Es ist in den Fällen, in denen die undefinierte Reihenfolge von HashSet Probleme bereitet hat, so gut wie ein kostenloses Upgrade.

Die "First-insertion-wins"-Regel

Die Reihenfolge wird beim ersten Einfügen eines Elements festgelegt. Ein bereits vorhandenes Element erneut hinzuzufügen, verschiebt es nicht:

Set<String> s = new LinkedHashSet<>();
s.add("a");
s.add("b");
s.add("c");
s.add("a");   // already present — returns false, order unchanged
System.out.println(s);   // [a, b, c]

Das macht es zum richtigen Werkzeug für "die Reihenfolge, in der Tags ankamen, merken" oder "eindeutige Ereignisse in zeitlicher Reihenfolge protokollieren." Wenn man ein Element entfernt und es erneut einfügt, wird es ans Ende der Liste gesetzt — die Position war an das aktuelle Einfügen gebunden, und das neue ist das einzige verbleibende.

Die Kosten: Zeiger und Zeiger

Die zusätzliche Sortierungsmechanik hat ihren Preis. Jeder Eintrag speichert nicht nur (hash, key, next-in-bucket) wie HashSet, sondern (hash, key, next-in-bucket, before, after). Das sind zwei zusätzliche Referenzen pro Element — ungefähr 16 Bytes extra auf einer 64-Bit-JVM. Bei einem Set mit 10 Millionen Long-Werten sind das rund 160 MB extra. Für den größten Teil des Anwendungscodes ist das nichts; bei Datenstrukturen in Cache-Größe spielt es jedoch eine Rolle.

Im Gegenzug erhält man O(1) für jede Operation (wie bei HashSet) plus eine stabile Iterationsreihenfolge, die weder vom Lastfaktor, noch vom Rehash, der Hash-Verteilung oder der JVM-Version abhängt.

Iterationskosten sind proportional zur Größe, nicht zur Kapazität

Es gibt einen subtilen Vorteil gegenüber HashSet: Das Durchlaufen eines LinkedHashSet folgt der verketteten Liste, sodass genau size Einträge besucht werden. Das Iterieren eines HashSet durchläuft jeden Bucket, also werden ungefähr capacity Slots besucht — einschließlich leerer. Bei einem spärlich befüllten Set kann das ein erheblicher Unterschied sein. Wenn man ein Set aufbaut, es weit über die zu behaltenden Elemente hinaus vergrößert und dann häufig iteriert, kann LinkedHashSet tatsächlich schneller iterieren.

Wann man es wählen sollte

Der Entscheidungsfluss:

  • Reihenfolge spielt keine Rolle, schnelle Mitgliedschaftsprüfung wird benötigtHashSet. Kleiner, einfacher.
  • Einfügereihenfolge soll beibehalten werdenLinkedHashSet. Gleiche Geschwindigkeit für add/contains, vorhersagbare Iteration.
  • Sortierte Reihenfolge wird gewünschtTreeSet. Anderer Algorithmus, Log-Zeit-Operationen.

Der häufigste Grund für LinkedHashSet ist defensiv: Man baut eine öffentliche API, die ein Set zurückgibt, und möchte nicht, dass Aufrufer von der willkürlichen Reihenfolge von HashSet abhängen. Ein LinkedHashSet ist das Freundlichste, was man zurückgeben kann — es hat denselben Vertrag wie ein Set, aber die Iteration ist reproduzierbar über Läufe und JVMs hinweg, was die für Benutzer sichtbare Ausgabe stabil und Tests leichter schreibbar macht.

Ein ausgearbeitetes Beispiel: eindeutige Tags in Ankunftsreihenfolge

Das folgende Programm baut zwei Sets aus demselben Strom von Tag-Eingaben: eines mit HashSet, eines mit LinkedHashSet. Die HashSet-Iterationsreihenfolge hängt von der JVM ab (sie ist stabil-aber-willkürlich für eine gegebene JVM); die LinkedHashSet-Reihenfolge ist genau die Reihenfolge, in der die eindeutigen Elemente zuerst erschienen. Dann zeigt es die "Entfernen und erneut Einfügen"-Regel und baut abschließend einen ordnungserhaltenden Deduplikator, der nur zwei Zeilen lang ist.

java— editable, runs on the server

Was man aus dem Lauf ablesen kann:

  • Das LinkedHashSet druckte die eindeutigen Ereignisse in der Reihenfolge aus, in der sie zuerst erschienen. Das HashSet druckte sie in einer ganz anderen Reihenfolge — was auch immer das Bucket-Layout vorgab.
  • Das erneute Einfügen von "a" änderte die Reihenfolge nicht. Das Entfernen und erneute Einfügen verschob es ans Ende. Das erste Einfügen verankert die Position.
  • Der ordnungserhaltende Deduplikator ist ein Einzeiler, sobald man den Trick kennt: In ein LinkedHashSet sammeln, dann zurück in eine Liste.
  • Der Scan der 10 Elemente durch ein LinkedHashSet mit 2 000 000 Buckets durchlief genau 10 Einträge; ein HashSet derselben Form hätte jeden leeren Bucket dazwischen gescannt.

Was als nächstes kommt

Die dritte Standard-Set-Implementierung bietet etwas, das weder HashSet noch LinkedHashSet kann: sortierte Iteration und die Möglichkeit, Bereichsabfragen zu stellen wie "jedes Tag zwischen a und m." Als nächstes kommt TreeSet.

Übungen

Übung
Was bietet `LinkedHashSet` im Vergleich zu einem einfachen `HashSet`?
Was bietet `LinkedHashSet` im Vergleich zu einem einfachen `HashSet`?
Was this page helpful?