Está usando java Map.containsKey () redundante ao usar map.get ()

90

Eu estive me perguntando por algum tempo se é permitido, dentro das melhores práticas, evitar o uso do containsKey()método emjava.util.Map e, vez disso, fazer uma verificação nula no resultado de get().

Meu raciocínio é que parece redundante fazer a pesquisa do valor duas vezes - primeiro para o containsKey() e depois novamente para get().

Por outro lado, pode ser que a maioria das implementações padrão de Mapcache da última consulta ou que o compilador possa eliminar a redundância e que, para legibilidade do código, seja preferível manter a containsKey()parte.

Eu apreciaria muito seus comentários.

Erik Madsen
fonte

Respostas:

112

Algumas implementações de mapa podem ter valores nulos, por exemplo, HashMap, neste caso se get(key)retorna nullnão garante que não haja nenhuma entrada no mapa associada a esta chave.

Então, se você quiser saber se um mapa contém uma chave, use Map.containsKey. Se você simplesmente precisa de um valor mapeado para um uso de chave Map.get(key). Se este mapa permitir valores nulos, um valor de retorno nulo não indica necessariamente que o mapa não contém mapeamento para a chave; Nesse caso, Map.containsKeyé inútil e afetará o desempenho. Além disso, no caso de acesso simultâneo a um mapa (por exemplo ConcurrentHashMap), após o teste, Map.containsKey(key)há uma chance de que a entrada seja removida por outro thread antes de você chamar Map.get(key).

Evgeniy Dorofeev
fonte
8
Mesmo se o valor for definido como null, você deseja tratar isso de forma diferente para uma chave / valor que não está definido? Se você não precisa especificamente tratá-lo de maneira diferente, você pode apenas usarget()
Peter Lawrey
1
Se Mapfor private, sua classe pode garantir que a nullnunca seja inserido no mapa. Nesse caso, você poderia usar get()seguido por uma verificação de nulo em vez de containsKey(). Isso pode ser mais claro e talvez um pouco mais eficiente em alguns casos.
Raedwald
44

Acho que é bastante normal escrever:

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

ao invés de

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

Não é menos legível e um pouco mais eficiente, então não vejo motivos para não fazê-lo. Obviamente, se seu mapa pode conter nulo, as duas opções não têm a mesma semântica .

assilias
fonte
8

Como indicaram as assilias, esta é uma questão semântica. Geralmente, Map.get (x) == null é o que você deseja, mas há casos em que é importante usar containsKey.

Um desses casos é um cache. Certa vez, trabalhei em um problema de desempenho em um aplicativo da web que consultava seu banco de dados com frequência em busca de entidades que não existiam. Quando estudei o código de cache para esse componente, percebi que ele estava consultando o banco de dados se cache.get (key) == null. Se o banco de dados retornasse nulo (entidade não encontrada), armazenaríamos em cache essa chave -> mapeamento nulo.

Mudar para containsKey resolveu o problema porque um mapeamento para um valor nulo realmente significava algo. O mapeamento de chave para null tinha um significado semântico diferente do da chave não existente.

Brandon
fonte
Interessante. Por que você simplesmente não adicionou uma verificação nula antes de armazenar os valores em cache?
Saket
Isso não mudaria nada. O ponto é que o mapeamento de chave para nulo significa "já fizemos isso. Está armazenado em cache. O valor é nulo". Versus não contém uma determinada chave, o que significa "Não sei, não está no cache, talvez seja necessário verificar o banco de dados."
Brandon
4
  • containsKeyseguido por a geté redundante apenas se soubermos a priori que valores nulos nunca serão permitidos. Se os valores nulos não forem válidos, a invocação de containsKeytem uma penalidade de desempenho não trivial e é apenas uma sobrecarga, conforme mostrado no benchmark abaixo.

  • Os Optionalidiomas do Java 8 - Optional.ofNullable(map.get(key)).ifPresentou Optional.ofNullable(map.get(key)).ifPresent- incorrem em uma sobrecarga não trivial em comparação com verificações de nulos simples.

  • A HashMapusa uma O(1)pesquisa de tabela constante, enquanto a TreeMapusa uma O(log(n))pesquisa. O containsKeyseguido por um getidioma é muito mais lento quando invocado em a TreeMap.

Benchmarks

Veja 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 (iterações) (lookupApproach) Modo Cnt Pontuação Unidades de erro

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

Referência de fonte TreeMap

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

Referência de fonte HashMap

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

Venkat Karun Venugopalan
fonte
3

Podemos tornar a resposta @assylias mais legível com Java8 opcional,

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

Em Java, se você verificar a implementação

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 usam getNode para recuperar a correspondência, onde o trabalho principal é feito.

a redundância é contextual, por exemplo, se você tiver um dicionário armazenado em um mapa hash. Quando você deseja recuperar o significado de uma palavra

fazendo ...

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

é redundante.

mas se você quiser verificar se uma palavra é válida ou não com base no dicionário. fazendo ...

 return dictionary.get(word) != null;

sobre...

 return dictionary.containsKey(word);

é redundante.

Se você verificar a implementação de HashSet , que usa HashMap internamente, use 'containsKey' no método 'contém'.

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