Utilise java Map.containsKey () redondant lors de l'utilisation de map.get ()

90

Je me demande depuis un certain temps s'il est permis, dans les meilleures pratiques, de s'abstenir d'utiliser la containsKey()méthode java.util.Mapet de faire une vérification nulle sur le résultat de get().

Mon raisonnement est qu'il semble redondant de faire la recherche de la valeur deux fois - d'abord pour le containsKey(), puis à nouveau pour get().

D'un autre côté, il se peut que la plupart des implémentations standard mettent en Mapcache la dernière recherche ou que le compilateur puisse autrement supprimer la redondance, et que pour la lisibilité du code, il est préférable de maintenir la containsKey()partie.

J'apprécierais beaucoup vos commentaires.

Erik Madsen
la source

Réponses:

112

Certaines implémentations de Map sont autorisées à avoir des valeurs nulles, par exemple HashMap, dans ce cas, si get(key)retourne, nullcela ne garantit pas qu'il n'y a pas d'entrée dans la carte associée à cette clé.

Donc, si vous voulez savoir si une carte contient une clé, utilisez Map.containsKey. Si vous avez simplement besoin d'une valeur mappée à une clé, utilisez Map.get(key). Si ce mappage autorise des valeurs nulles, alors une valeur de retour nulle n'indique pas nécessairement que le mappage ne contient aucun mappage pour la clé; Dans un tel cas, cela Map.containsKeyne sert à rien et affectera les performances. De plus, en cas d'accès simultané à une carte (par exemple ConcurrentHashMap), après le test, Map.containsKey(key)il y a une chance que l'entrée soit supprimée par un autre thread avant d'appeler Map.get(key).

Evgeniy Dorofeev
la source
8
Même si la valeur est définie sur null, voulez-vous traiter cela différemment en une clé / valeur qui n'est pas définie? Si vous n'avez pas spécifiquement besoin de le traiter différemment, vous pouvez simplement utiliserget()
Peter Lawrey
1
Si Mapc'est le cas private, votre classe pourra peut-être garantir qu'un nulln'est jamais inséré dans la carte. Dans ce cas, vous pouvez utiliser get()suivi d'une vérification de null au lieu de containsKey(). Cela peut être plus clair, et peut-être un peu plus efficace, dans certains cas.
Raedwald
44

Je pense que c'est assez standard d'écrire:

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

au lieu de

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

Ce n'est pas moins lisible et un peu plus efficace donc je ne vois aucune raison de ne pas le faire. Évidemment, si votre carte peut contenir null, les deux options n'ont pas la même sémantique .

assylies
la source
8

Comme l'a indiqué assylias, c'est une question sémantique. Généralement, Map.get (x) == null est ce que vous voulez, mais il y a des cas où il est important d'utiliser containsKey.

Un tel cas est un cache. J'ai déjà travaillé sur un problème de performances dans une application Web qui interrogeait fréquemment sa base de données à la recherche d'entités qui n'existaient pas. Lorsque j'ai étudié le code de mise en cache pour ce composant, j'ai réalisé qu'il interrogeait la base de données si cache.get (clé) == null. Si la base de données renvoyait null (entité non trouvée), nous mettrions en cache cette clé -> mappage nul.

Le passage à containsKey a résolu le problème car un mappage à une valeur nulle signifiait en fait quelque chose. Le mappage de clé à null avait une signification sémantique différente de celle de la clé inexistante.

Brandon
la source
Intéressant. Pourquoi n'avez-vous pas simplement ajouté une vérification nulle avant de mettre les valeurs en cache?
Saket
Cela ne changerait rien. Le fait est que le mappage de clé sur null signifie "nous l'avons déjà fait. Il est mis en cache. La valeur est nulle". Par rapport à ne pas contenir du tout une clé donnée, ce qui signifie «Je ne sais pas, pas dans le cache, nous devrons peut-être vérifier la base de données».
Brandon
4
  • containsKeysuivi de a getn'est redondant que si nous savons apriori que les valeurs nulles ne seront jamais autorisées. Si les valeurs nulles ne sont pas valides, l'appel de containsKeya une pénalité de performances non négligeable et est juste une surcharge, comme indiqué dans le test ci-dessous.

  • Les Optionalidiomes Java 8 - Optional.ofNullable(map.get(key)).ifPresentou Optional.ofNullable(map.get(key)).ifPresent- entraînent une surcharge non triviale par rapport aux vérifications nulles simples.

  • A HashMaputilise une O(1)recherche de table constante tandis que a TreeMaputilise une O(log(n))recherche. Le containsKeysuivi d'un getidiome est beaucoup plus lent lorsqu'il est invoqué sur un TreeMap.

Benchmarks

Voir 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 (itérations) (lookupApproach) Mode Cnt Score Unités d'erreur

MultihashTypeLookupBenchmark.testLookup 1000 t1 moyenne 9 33.438 ± 4.514 us / op
MultihashTypeLookupBenchmark.testLookup 1000 t2 moyenne 9 26,986 ± 0,405 us / op
MultihashTypeLookupBenchmark.testLookup 1000 t3 moyenne 9 39,259 ± 1,306 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h1 moyenne 9 18,954 ± 0,414 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h2 moyenne 9 15,486 ± 0,395 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h3 moy 9 16.780 ± 0.719 us / op

Référence source TreeMap

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

Référence de la source HashMap

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

Venkat Karun Venugopalan
la source
3

Nous pouvons rendre la réponse @assylias plus lisible avec Java8 facultatif,

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

En Java si vous vérifiez l'implémentation

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

les deux utilisent getNode pour récupérer la correspondance, là où le travail principal est effectué.

la redondance est contextuelle, par exemple si vous avez un dictionnaire stocké dans une carte de hachage. Lorsque vous souhaitez récupérer la signification d'un mot

Faire...

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

est redondant.

mais si vous voulez vérifier qu'un mot est valide ou non basé sur le dictionnaire. Faire...

 return dictionary.get(word) != null;

plus de...

 return dictionary.containsKey(word);

est redondant.

Si vous vérifiez l' implémentation de HashSet , qui utilise HashMap en interne, utilisez 'containsKey' dans la méthode 'contains'.

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