Utilizza java Map.containsKey () ridondante quando si utilizza map.get ()


90

Mi chiedo da tempo se sia consentito, all'interno delle migliori pratiche, astenersi dall'utilizzare il containsKey()metodo java.util.Mape invece eseguire un controllo nullo sul risultato da get().

La mia logica è che sembra ridondante cercare il valore due volte: prima per containsKey()e poi di nuovo per get().

D'altra parte può darsi che la maggior parte delle implementazioni standard della Mapcache l'ultima ricerca o che il compilatore possa altrimenti eliminare la ridondanza, e che per la leggibilità del codice è preferibile mantenere la containsKey()parte.

Apprezzerei molto i tuoi commenti.

Risposte:


112

Alcune implementazioni della mappa possono avere valori nulli, ad esempio HashMap, in questo caso se get(key)restituisce nullnon garantisce che non ci siano voci nella mappa associate a questa chiave.

Quindi, se vuoi sapere se una mappa contiene una chiave, usa Map.containsKey. Se hai semplicemente bisogno di un valore associato a una chiave, usa Map.get(key). Se questa mappa consente valori null, un valore restituito null non indica necessariamente che la mappa non contiene alcuna mappatura per la chiave; In tal caso Map.containsKeyè inutile e influirà sulle prestazioni. Inoltre, in caso di accesso simultaneo a una mappa (ad esempio ConcurrentHashMap), dopo il test Map.containsKey(key)c'è la possibilità che la voce venga rimossa da un altro thread prima della chiamata Map.get(key).


8
Anche se il valore è impostato su null, vuoi trattarlo diversamente da una chiave / valore che non è impostato? Se non hai specificamente bisogno di trattarlo in modo diverso, puoi semplicemente usareget()
Peter Lawrey,

1
Se lo Mapè private, la tua classe potrebbe garantire che nullnon venga mai inserita nella mappa. In quel caso, potresti usare get()seguito da un controllo per null invece di containsKey(). Ciò può essere più chiaro e forse un po 'più efficiente, in alcuni casi.
Raedwald

44

Penso che sia abbastanza standard scrivere:

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

invece di

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

Non è meno leggibile e leggermente più efficiente, quindi non vedo ragioni per non farlo. Ovviamente se la tua mappa può contenere null, le due opzioni non hanno la stessa semantica .


8

Come indicato da Assylias, questa è una questione semantica. Generalmente, Map.get (x) == null è quello che vuoi, ma ci sono casi in cui è importante usare containsKey.

Uno di questi casi è una cache. Una volta ho lavorato su un problema di prestazioni in un'app Web che interrogava il suo database spesso alla ricerca di entità che non esistevano. Quando ho studiato il codice di memorizzazione nella cache per quel componente, mi sono reso conto che stava interrogando il database se cache.get (key) == null. Se il database restituisce null (entità non trovata), memorizzeremo nella cache quella chiave -> mappatura nulla.

Il passaggio a containsKey ha risolto il problema perché una mappatura a un valore nullo significava effettivamente qualcosa. La mappatura della chiave su null aveva un significato semantico diverso rispetto alla chiave non esistente.


Interessante. Perché non hai semplicemente aggiunto un controllo nullo prima di memorizzare i valori nella cache?
Saket

Questo non cambierebbe nulla. Il punto è che la mappatura della chiave su null significa "l'abbiamo già fatto. È memorizzato nella cache. Il valore è null". Rispetto a non contenere affatto una determinata chiave, il che significa "Non so, non nella cache, potrebbe essere necessario controllare il DB".
Brandon

4
  • containsKeyseguito da a getè ridondante solo se sappiamo apriori che i valori nulli non saranno mai consentiti. Se i valori null non sono validi, l'invocazione di containsKeyha una riduzione delle prestazioni non banale ed è solo un sovraccarico, come mostrato nel benchmark di seguito.

  • Gli Optionalidiomi Java 8 - Optional.ofNullable(map.get(key)).ifPresento Optional.ofNullable(map.get(key)).ifPresent- incorrono in un overhead non banale rispetto ai soli controlli null vanilla.

  • A HashMaputilizza una O(1)ricerca costante nella tabella mentre a TreeMaputilizza una O(log(n))ricerca. Il containsKeyseguito da un getidioma è molto più lento se invocato su un TreeMap.

Punti di riferimenti

Vedi 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 (iterazioni) (lookupApproach) Modalità 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

Riferimento sorgente TreeMap

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

Riferimento sorgente HashMap

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


3

Possiamo rendere la risposta di @assylias più leggibile con Java8 Opzionale,

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

2

In Java se controlli l'implementazione

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;
}

entrambi usano getNode per recuperare la corrispondenza, dove viene svolto il lavoro principale.

la ridondanza è contestuale, ad esempio, se hai un dizionario memorizzato in una mappa hash. Quando vuoi recuperare il significato di una parola

facendo ...

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

è ridondante.

ma se vuoi controllare che una parola sia valida o meno in base al dizionario. facendo ...

 return dictionary.get(word) != null;

al di sopra di...

 return dictionary.containsKey(word);

è ridondante.

Se controlli l' implementazione di HashSet , che utilizza internamente HashMap, utilizza il metodo "containsKey" nel metodo "contains".

    public boolean contains(Object o) {
        return map.containsKey(o);
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.