当使用map.get()时,是否使用java Map.containsKey()是多余的?

109
我一直在思考,在最佳实践中,是否允许不使用java.util.Map上的containsKey()方法,而是对get()的结果进行空值检查。
我的理由是,这样做似乎重复了两次查找值--首先是containsKey(),然后是get()
另一方面,可能大多数标准的Map实现会缓存最后一次查找或者编译器可以消除冗余。为了代码可读性,保留containsKey()部分可能更好。
非常感谢您的意见。
6个回答

128

一些 Map 实现允许有空值存在,例如 HashMap,此时如果 get(key) 返回 null,并不保证这个 key 没有与之关联的 entry。

因此,如果你想知道一个 Map 是否包含某个 key,请使用 Map.containsKey。如果你只需要获取 key 对应的 value,请使用 Map.get(key)。如果这个 Map 允许 null 值存在,那么返回 null 并不一定表示 Map 中不存在该 key 的映射;在这种情况下,Map.containsKey 是没用的,并且会影响性能。此外,在对 Map 进行并发访问的情况下(例如 ConcurrentHashMap),在测试完 Map.containsKey(key) 后,有可能在调用 Map.get(key) 之前另一个线程已经将 entry 删除了。


9
即使把值设为 null,你是否想要将其与未设置的键/值区别对待?如果您不需要特别区分它们,可以直接使用 get() - Peter Lawrey
1
如果你的Mapprivate的,你的类可能能够保证在map中永远不会插入null。在这种情况下,你可以使用get()并检查null是否存在,而不是使用containsKey()。在某些情况下,这样做可能更清晰、更高效。 - Raedwald

50

我认为写成以下形式是相当标准的:

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

而不是

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

它并不会使内容难以阅读,而且稍微更加高效,所以我认为没有理由不这样做。显然如果你的映射可以包含null,则这两个选项的语义不同。


9
如assylias所说,这是一个语义问题。一般来说,Map.get(x) == null是您想要的,但有些情况下使用containsKey很重要。
其中一个例子是缓存。我曾经处理过一个性能问题,这个Web应用程序频繁查询数据库以查找不存在的实体。当我研究该组件的缓存代码时,发现它在cache.get(key) == null时查询数据库。如果数据库返回null(未找到实体),我们会将该键-值映射缓存到null。
切换到containsKey解决了此问题,因为映射到null值实际上意味着某些含义。映射到null的键具有与不存在键不同的语义含义。

有趣。为什么你不在缓存值之前添加一个空检查呢? - Saket
那不会改变任何事情。关键是将映射到null的键表示“我们已经完成了这个操作。它已经被缓存了。值 null”。与根本不包含给定键的情况不同,后者表示“不知道,在缓存中不存在,我们可能需要检查数据库。” - Brandon
将 containsKey 替换为 Map.get(x) == null 条件解决了我的 Sonar 扫描问题,而且没有破坏现有的 TCs,所以谢谢。 - iamjoshua

6
  • 如果我们事先知道空值将不会被允许,那么使用containsKey后跟get是多余的。如果空值无效,调用containsKey将带来非常显著的性能损失,并且只是开销,如下面的基准测试所示。

  • 与仅使用普通的null检查相比,Java 8 Optional惯用法 - Optional.ofNullable(map.get(key)).ifPresentOptional.ofNullable(map.get(key)).ifPresent - 需要承担非常显著的开销。

  • HashMap 使用 O(1)的常数表查找,而 TreeMap使用O(log(n))的查找。在TreeMap上调用containsKey并跟随get惯用法时,速度慢得多。

基准测试

请参见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));
}
基准测试                                 (迭代次数)  (查找方法)  模式  Cnt   分数   误差  单位
MultihashTypeLookupBenchmark.testLookup 1000 t1 avgt 9 33.438 ± 4.514 微秒/操作 MultihashTypeLookupBenchmark.testLookup 1000 t2 avgt 9 26.986 ± 0.405 微秒/操作 MultihashTypeLookupBenchmark.testLookup 1000 t3 avgt 9 39.259 ± 1.306 微秒/操作 MultihashTypeLookupBenchmark.testLookup 1000 h1 avgt 9 18.954 ± 0.414 微秒/操作 MultihashTypeLookupBenchmark.testLookup 1000 h2 avgt 9 15.486 ± 0.395 微秒/操作 MultihashTypeLookupBenchmark.testLookup 1000 h3 avgt 9 16.780 ± 0.719 微秒/操作

TreeMap源代码参考

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

HashMap源代码参考

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


5
我们可以使用Java8 Optional使@assylias的答案更易读,
Optional.ofNullable(map.get(key)).ifPresent(value -> {
     //do something with value
};)

3

如果你查看Java的实现

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

两者都使用getNode来检索匹配项,主要工作在此处完成。

冗余性是上下文相关的,例如如果你在哈希映射中存储了一个字典。当你想要检索一个单词的含义时

正在进行...

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

是多余的。

但如果您想根据字典检查单词是否有效。 进行中...

 return dictionary.get(word) != null;

超过...

 return dictionary.containsKey(word);

是多余的。

如果您查看HashSet实现,它在内部使用HashMap,使用'containsKey'方法来实现'contains'方法。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接