Ist die Verwendung von Java Map.containsKey () bei Verwendung von map.get () redundant?

90

Ich habe mich seit einiger Zeit gefragt, ob es im Rahmen der Best Practice zulässig ist, die containsKey()Methode nicht zu verwenden java.util.Mapund stattdessen eine Nullprüfung des Ergebnisses von durchzuführen get().

Mein Grundprinzip ist, dass es überflüssig erscheint, den Wert zweimal nachzuschlagen - zuerst für containsKey()und dann wieder für get().

Andererseits kann es sein, dass die meisten Standardimplementierungen des MapCaches die letzte Suche durchführen oder dass der Compiler die Redundanz auf andere Weise beseitigen kann und dass es für die Lesbarkeit des Codes vorzuziehen ist, das containsKey()Teil beizubehalten .

Ich würde mich sehr über Ihre Kommentare freuen.

Erik Madsen
quelle

Antworten:

110

Einige Map-Implementierungen dürfen Nullwerte haben, z. B. HashMap. In diesem Fall kann bei get(key)Rückgabe nullnicht garantiert werden, dass mit diesem Schlüssel kein Eintrag in der Map verknüpft ist.

Wenn Sie also wissen möchten, ob eine Karte eine Schlüsselverwendung enthält Map.containsKey. Wenn Sie lediglich einen Wert benötigen, der einem Schlüssel zugeordnet ist, verwenden Sie Map.get(key). Wenn diese Zuordnung Nullwerte zulässt, bedeutet ein Rückgabewert von Null nicht unbedingt, dass die Zuordnung keine Zuordnung für den Schlüssel enthält. In diesem Fall Map.containsKeyist nutzlos und beeinträchtigt die Leistung. Darüber hinaus besteht bei gleichzeitigem Zugriff auf eine Karte (z. B. ConcurrentHashMap) nach dem Testen die Map.containsKey(key)Möglichkeit, dass der Eintrag vor dem Aufruf von einem anderen Thread entfernt wird Map.get(key).

Evgeniy Dorofeev
quelle
8
Möchten Sie das, auch wenn der Wert auf gesetzt ist null, anders behandeln als einen Schlüssel / Wert, der nicht gesetzt ist? Wenn Sie es nicht speziell anders behandeln müssen, können Sie es einfach verwendenget()
Peter Lawrey
1
Wenn dies der Fall Mapist private, kann Ihre Klasse möglicherweise garantieren, dass a nullniemals in die Karte eingefügt wird. In diesem Fall können Sie get()stattdessen eine Überprüfung auf null anstelle von verwenden containsKey(). Dies kann in einigen Fällen klarer und vielleicht etwas effizienter sein.
Raedwald
43

Ich denke, es ist ziemlich normal zu schreiben:

Object value = map.get(key);
if (value != null) {
    //do something with value
}

anstatt

if (map.containsKey(key)) {
    Object value = map.get(key);
    //do something with value
}

Es ist nicht weniger lesbar und etwas effizienter, daher sehe ich keine Gründe, es nicht zu tun. Wenn Ihre Karte null enthalten kann, haben die beiden Optionen natürlich nicht dieselbe Semantik .

Assylien
quelle
8

Wie Assylias angedeutet hat, ist dies eine semantische Frage. Im Allgemeinen ist Map.get (x) == null das, was Sie wollen, aber es gibt Fälle, in denen es wichtig ist, includesKey zu verwenden.

Ein solcher Fall ist ein Cache. Ich habe einmal an einem Leistungsproblem in einer Web-App gearbeitet, die ihre Datenbank häufig nach Entitäten abfragte, die es nicht gab. Als ich den Caching-Code für diese Komponente studierte, stellte ich fest, dass die Datenbank abgefragt wurde, wenn cache.get (key) == null. Wenn die Datenbank null zurückgibt (Entität nicht gefunden), würden wir diesen Schlüssel zwischenspeichern -> null Zuordnung.

Der Wechsel zu includesKey löste das Problem, da eine Zuordnung zu einem Nullwert tatsächlich etwas bedeutete. Die Schlüsselzuordnung zu null hatte eine andere semantische Bedeutung als der nicht vorhandene Schlüssel.

Brandon
quelle
Interessant. Warum haben Sie vor dem Zwischenspeichern von Werten nicht einfach eine Nullprüfung hinzugefügt?
Saket
Das würde nichts ändern. Der Punkt ist, dass die Schlüsselzuordnung zu null bedeutet "wir haben dies bereits getan. Es wird zwischengespeichert. Der Wert ist null". Versus enthält überhaupt keinen bestimmten Schlüssel, was bedeutet "Weiß nicht, nicht im Cache, wir müssen möglicherweise die Datenbank überprüfen."
Brandon
4
  • containsKeygefolgt von a getist nur dann redundant, wenn wir apriori wissen, dass Nullwerte niemals zulässig sind. Wenn Nullwerte nicht gültig sind, hat der Aufruf von containsKeyeine nicht triviale Leistungseinbuße und ist nur ein Overhead, wie in der folgenden Benchmark gezeigt.

  • Die Java 8- OptionalRedewendungen - Optional.ofNullable(map.get(key)).ifPresentoder Optional.ofNullable(map.get(key)).ifPresent- verursachen einen nicht trivialen Overhead im Vergleich zu nur Vanille-Null-Prüfungen.

  • A HashMapverwendet eine O(1)konstante Tabellensuche, während a TreeMapeine O(log(n))Suche verwendet. Das containsKeygefolgt von einer getRedewendung ist viel langsamer, wenn es auf a aufgerufen wird TreeMap.

Benchmarks

Siehe https://github.com/vkarun/enum-reverse-lookup-table-jmh

// t1
static Type lookupTreeMapNotContainsKeyThrowGet(int t) {
  if (!lookupT.containsKey(t))
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return lookupT.get(t);
}
// t2
static Type lookupTreeMapGetThrowIfNull(int t) {
  Type type = lookupT.get(t);
  if (type == null)
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return type;
}
// t3
static Type lookupTreeMapGetOptionalOrElseThrow(int t) {
  return Optional.ofNullable(lookupT.get(t)).orElseThrow(() -> new 
      IllegalStateException("Unknown Multihash type: " + t));
}
// h1
static Type lookupHashMapNotContainsKeyThrowGet(int t) {
  if (!lookupH.containsKey(t))
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return lookupH.get(t);
}
// h2
static Type lookupHashMapGetThrowIfNull(int t) {
  Type type = lookupH.get(t);
  if (type == null)
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return type;
}
// h3
static Type lookupHashMapGetOptionalOrElseThrow(int t) {
  return Optional.ofNullable(lookupH.get(t)).orElseThrow(() -> new 
    IllegalStateException("Unknown Multihash type: " + t));
}
Benchmark (Iterationen) (lookupApproach) Modus Cnt Score Error Units

MultihashTypeLookupBenchmark.testLookup 1000 t1 avgt 9 33.438 ± 4.514 us / op
MultihashTypeLookupBenchmark.testLookup 1000 t2 avgt 9 26.986 ± 0.405 us / op
MultihashTypeLookupBenchmark.testLookup 1000 t3 avgt 9 39,259 ± 1,306 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h1 avgt 9 18.954 ± 0.414 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h2 avgt 9 15.486 ± 0.395 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h3 avgt 9 16.780 ± 0.719 us / op

TreeMap-Quellreferenz

https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java

HashMap-Quellreferenz

https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/HashMap.java

Venkat Karun Venugopalan
quelle
3

Wir können @assylias Antwort mit Java8 lesbarer machen. Optional,

Optional.ofNullable(map.get(key)).ifPresent(value -> {
     //do something with value
};)
Raja
quelle
2

In Java, wenn Sie die Implementierung überprüfen

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

Beide verwenden getNode, um den Abgleich abzurufen, in dem die Hauptarbeit erledigt wird.

Redundanz ist kontextabhängig, wenn Sie beispielsweise ein Wörterbuch in einer Hash-Map gespeichert haben. Wenn Sie die Bedeutung eines Wortes abrufen möchten

tun...

if(dictionary.containsKey(word)) {
   return dictionary.get(word);
}

ist redundant.

Wenn Sie jedoch überprüfen möchten, ob ein Wort gültig ist oder nicht, basierend auf dem Wörterbuch. tun...

 return dictionary.get(word) != null;

Über...

 return dictionary.containsKey(word);

ist redundant.

Wenn Sie die HashSet- Implementierung überprüfen , bei der HashMap intern verwendet wird, verwenden Sie ' includesKey ' in der Methode 'enthält'.

    public boolean contains(Object o) {
        return map.containsKey(o);
    }
asela38
quelle