Está usando java Map.containsKey () redundante cuando usa map.get ()

93

Me he estado preguntando durante algún tiempo si está permitido dentro de las mejores prácticas abstenerse de usar el containsKey()método java.util.Mapy, en su lugar, hacer una verificación nula en el resultado de get().

Mi razón fundamental es que parece redundante realizar la búsqueda del valor dos veces, primero para el containsKey()y luego nuevamente para get().

Por otro lado, puede ser que la mayoría de las implementaciones estándar de Mapcaché la última búsqueda o que el compilador pueda eliminar la redundancia, y que para la legibilidad del código es preferible mantener la containsKey()parte.

Agradecería mucho sus comentarios.

Erik Madsen
fuente

Respuestas:

112

Algunas implementaciones de mapas pueden tener valores nulos, por ejemplo, HashMap, en este caso, si se get(key)devuelve null, no garantiza que no haya una entrada en el mapa asociada con esta clave.

Entonces, si desea saber si un mapa contiene un uso clave Map.containsKey. Si simplemente necesita un valor asignado a un uso clave Map.get(key). Si este mapa permite valores nulos, entonces un valor de retorno de nulo no indica necesariamente que el mapa no contiene ningún mapeo para la clave; En tal caso, Map.containsKeyes inútil y afectará al rendimiento. Además, en caso de acceso simultáneo a un mapa (p ConcurrentHashMap. Ej. ), Después de realizar la prueba, Map.containsKey(key)existe la posibilidad de que otro hilo elimine la entrada antes de llamar Map.get(key).

Evgeniy Dorofeev
fuente
8
Incluso si el valor está establecido en null, ¿desea tratar eso de manera diferente a una clave / valor que no está establecido? Si no necesita tratarlo específicamente de manera diferente, simplemente puede usarget()
Peter Lawrey
1
Si Mapes tuyo private, tu clase podría garantizar nullque nunca se inserte en el mapa. En ese caso, puede usar get()seguido de una comprobación de nulo en lugar de containsKey(). Hacerlo puede ser más claro, y quizás un poco más eficiente, en algunos casos.
Raedwald
44

Creo que es bastante estándar escribir:

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

en vez de

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

No es menos legible y un poco más eficiente, por lo que no veo ninguna razón para no hacerlo. Obviamente, si su mapa puede contener nulos, las dos opciones no tienen la misma semántica .

Assylias
fuente
8

Como indicó Assylias, esta es una cuestión semántica. Generalmente, Map.get (x) == null es lo que desea, pero hay casos en los que es importante usar containsKey.

Uno de esos casos es un caché. Una vez trabajé en un problema de rendimiento en una aplicación web que consultaba su base de datos con frecuencia buscando entidades que no existían. Cuando estudié el código de almacenamiento en caché para ese componente, me di cuenta de que estaba consultando la base de datos si cache.get (key) == null. Si la base de datos devolvió nulo (entidad no encontrada), almacenaríamos en caché esa clave -> asignación nula.

Cambiar a containsKey resolvió el problema porque una asignación a un valor nulo en realidad significaba algo. La asignación de claves a nulo tenía un significado semántico diferente al de la clave que no existía.

Brandon
fuente
Interesante. ¿Por qué no agregó simplemente una verificación nula antes de almacenar en caché los valores?
Saket
Eso no cambiaría nada. El punto es que la asignación de claves a nulo significa "ya lo hemos hecho. Está almacenado en caché. El valor es nulo". Versus no contiene una clave determinada, lo que significa "No lo sé, no en la caché, es posible que debamos verificar la base de datos".
Brandon
5
  • containsKeyseguido de a getes redundante solo si sabemos a priori que nunca se permitirán valores nulos. Si los valores nulos no son válidos, la invocación de containsKeytiene una penalización de rendimiento no trivial y es solo una sobrecarga, como se muestra en el punto de referencia a continuación.

  • Los Optionalmodismos de Java 8 , Optional.ofNullable(map.get(key)).ifPresento bien Optional.ofNullable(map.get(key)).ifPresent, incurren en una sobrecarga no trivial en comparación con las comprobaciones nulas de vainilla.

  • A HashMapusa una O(1)búsqueda de tabla constante mientras que a TreeMapusa una O(log(n))búsqueda. El containsKeyseguido de un getmodismo es mucho más lento cuando se invoca en un TreeMap.

Benchmarks

Ver 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 (iteraciones) (lookupApproach) Mode 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

Referencia de la fuente TreeMap

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

Referencia de origen de HashMap

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

Venkat Karun Venugopalan
fuente
3

Podemos hacer que la respuesta de @assylias sea más legible con Java8 Opcional,

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

En Java si verificas la implementación

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

ambos usan getNode para recuperar la coincidencia, donde se realiza el trabajo principal.

la redundancia es contextual, por ejemplo, si tiene un diccionario almacenado en un mapa hash. Cuando quieres recuperar el significado de una palabra

haciendo...

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

es redundante.

pero si quieres comprobar si una palabra es válida o no está basada en el diccionario. haciendo...

 return dictionary.get(word) != null;

terminado...

 return dictionary.containsKey(word);

es redundante.

Si verifica la implementación de HashSet , que usa HashMap internamente, use 'containsKey' en el método 'contains'.

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