Leistungsüberlegungen für keySet () und entrySet () von Map

76

Alle,

Kann mir bitte jemand genau mitteilen, was die Leistungsprobleme zwischen den beiden sind? Die Site: CodeRanch bietet einen kurzen Überblick über die internen Aufrufe, die bei Verwendung von keySet () und get () erforderlich wären. Es wäre jedoch großartig, wenn jemand genaue Angaben zum Ablauf machen könnte, wenn die Methoden keySet () und get () verwendet werden. Dies würde mir helfen, die Leistungsprobleme besser zu verstehen.

name_masked
quelle

Antworten:

70

Dies hängt zunächst ganz davon ab, welchen Kartentyp Sie verwenden. Da der JavaRanch-Thread jedoch über HashMap spricht, gehe ich davon aus, dass dies die Implementierung ist, auf die Sie sich beziehen. Nehmen wir auch an, Sie sprechen von der Standard-API-Implementierung von Sun / Oracle.

Zweitens, wenn Sie sich Sorgen über die Leistung beim Durchlaufen Ihrer Hash-Map machen, sollten Sie sich diese ansehen LinkedHashMap. Aus den Dokumenten:

Das Durchlaufen der Sammlungsansichten einer LinkedHashMap erfordert unabhängig von ihrer Kapazität Zeit, die proportional zur Größe der Karte ist. Die Iteration über eine HashMap ist wahrscheinlich teurer und erfordert Zeit proportional zu ihrer Kapazität.

HashMap.entrySet ()

Der Quellcode für diese Implementierung ist verfügbar. Die Implementierung gibt im Grunde nur eine neue zurück HashMap.EntrySet. Eine Klasse, die so aussieht:

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator(); // returns a HashIterator...
    }
    // ...
}

und HashIteratorsieht aus wie

private abstract class HashIterator<E> implements Iterator<E> {
    Entry<K,V> next;    // next entry to return
    int expectedModCount;   // For fast-fail
    int index;      // current slot
    Entry<K,V> current; // current entry

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }

    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    current = e;
        return e;
    }

    // ...
}

Da haben Sie es also ... Das ist der Code, der vorschreibt, was passieren wird, wenn Sie ein entrySet durchlaufen. Es geht durch das gesamte Array, das so lang ist wie die Kartenkapazität.

HashMap.keySet () und .get ()

Hier müssen Sie zuerst den Schlüsselsatz beschaffen. Dies benötigt Zeit proportional zur Kapazität der Karte (im Gegensatz zur Größe für die LinkedHashMap). Danach rufen Sie get()für jede Taste einmal auf. Sicher, im Durchschnitt dauert dies bei einer guten HashCode-Implementierung eine konstante Zeit. Allerdings wird es zwangsläufig viele erfordern .hashCodeund .equalsAnrufe, die offensichtlich mehr Zeit in Anspruch nehmen als nur einen tun entry.value()Anruf.

aioobe
quelle
1
+1 "Die Iteration über die Sammlungsansichten einer LinkedHashMap erfordert Zeit proportional zur Größe der Karte, unabhängig von ihrer Kapazität. Die Iteration über eine HashMap ist wahrscheinlich teurer und erfordert Zeit proportional zu ihrer Kapazität."
Metdos
Wenn Sie jedoch nur auf die Schlüssel oder nur auf die Werte der Map zugreifen müssen, ziehen Sie es vor, die von keySet () und Collection zurückgegebenen Werte () zu wiederholen. Ein weiterer Punkt: Das von keySet () zurückgegebene Set und die von values ​​() zurückgegebene Collection werden beide von der ursprünglichen Map unterstützt. Das heißt, wenn Sie Änderungen an ihnen vornehmen, werden diese wieder in der Map angezeigt. Beide unterstützen jedoch nicht die Methoden add () und addAll (), dh Sie können dem Set oder dem neuen Wert keinen neuen Schlüssel hinzufügen in der Sammlung.
Sactiw
@aioobe AS Sie haben geschrieben "Das ist der Code, der vorschreibt, was passieren wird, wenn Sie ein entrySet durchlaufen. Er durchläuft das gesamte Array, das so lang ist wie die Kapazität der Karte." Sollte es nicht "..... was ist so lang wie die Größe der Karte" sein?
Sumit Kumar Saha
Gute gute Antwort. Ich beziehe mich immer lieber auf den Quellcode, da er die ultimative Quelle der Wahrheit ist
ACV
75

Der häufigste Fall, in dem die Verwendung von entrySet gegenüber keySet vorzuziehen ist, besteht darin, dass Sie alle Schlüssel / Wert-Paare in einer Map durchlaufen.

Das ist effizienter:

for (Map.Entry entry : map.entrySet()) {
    Object key = entry.getKey();
    Object value = entry.getValue();
}

als:

for (Object key : map.keySet()) {
    Object value = map.get(key);
}

Denn im zweiten Fall wird für jeden Schlüssel im keySet die map.get()Methode aufgerufen, die - im Fall einer HashMap - erfordert, dass die hashCode()und equals()-Methoden des Schlüsselobjekts ausgewertet werden, um den zugehörigen Wert * zu finden. Im ersten Fall entfällt diese zusätzliche Arbeit.

Bearbeiten: Dies ist noch schlimmer, wenn Sie eine TreeMap in Betracht ziehen, bei der ein abzurufender Aufruf O (log2 (n)) ist, dh der Komparator für Testament muss möglicherweise log2 (n) Mal (n = Größe der Karte) ausführen, bevor er gefunden wird der zugehörige Wert.

* Einige Map-Implementierungen verfügen über interne Optimierungen, die die Identität der Objekte vor dem hashCode()und überprüfen equals().

Michael Barker
quelle
3
Wenn es sich bei der Karte um eine TreeMap und nicht um eine HashMap handelt, get()handelt es sich außerdem um eine O(log(n))Operation.
ILMTitan
@ILMIian und Michael: Warum der Unterschied zwischen TreeMap und HashMap?
name_masked
TreeMap und HashMap sind unterschiedliche Datenstrukturen. TreeMap basiert auf einem Rot / Schwarz-Baum. Eine HashMap ist eine Bucket- und Listen-Hashtabelle. In beiden Fällen ist der Aufruf von get () nicht kostenlos und seine Kosten hängen von der Art der Datenstruktur ab.
Michael Barker
1
Java 8 (und höher) den Wert in HashMapimplementiert als binäre Suchbaum statt LinkedList. Siehe openjdk.java.net/jeps/180
Anfänger
14

Hier ist der Link zu einem Artikel , in dem die Leistung von und verglichen entrySet()wird , sowie Ratschläge, wann die einzelnen Ansätze verwendet werden sollen.keySet()values()

Anscheinend ist die Verwendung von keySet()schneller (außer bequemer) als entrySet()solange Sie Map.get()die Werte nicht benötigen .

Stefan L.
quelle
1
Sie sagten in diesem Artikel: "Diese Methode (mit keySet oder Werten anstelle von entrySet) bietet einen leichten Leistungsvorteil gegenüber der entrySet-Iteration (ca. 10% schneller) und ist sauberer." Darf ich wissen, wie Sie diesen Wert von "10%" erhalten haben? Sie haben weder Messungen noch externe Daten angezeigt, die einen solchen Wert enthalten.
Dantuch
@dantuch Ich bin nicht Sergiy, aber der Artikel macht meiner Meinung nach Sinn. Der Artikel ist jedoch ab 2008 alt. Sie können jederzeit einen Mikrobenchmark mit Googles Caliper erstellen, z. B. für das neueste JDK. Wenn Sie neugierig sind, veröffentlichen Sie bitte die Ergebnisse.
Stefan L