为什么不常见的Map实现为Map.get()缓存Map.containsKey()的结果

时间:2014-05-02 15:31:48

标签: java collections guava short-circuiting

带有地图的常见模式是检查某个密钥是否存在,然后仅在该值处理时才采取行动,请考虑:

if(!map.containsKey(key)) {
  map.put(key, new DefaultValue());
}
return map.get(key);

然而,这通常被认为是差的,因为它需要两个地图查找,其中这个替代方案只需要一个:

Value result = map.get(key);
if(result == null) 
{
  result = new DefaultValue();
  map.put(key,result);
}
return result;

然而,这第二个实施有其自身的问题。除了不那么简洁和可读之外,它可能是不正确的,因为它无法区分密钥不存在的情况和密钥存在的情况,但明确地映射到null。在个别情况下,我们当然可以创建外部不变量,地图不会包含null值,但一般来说,我们不能依赖第二种模式,需要回退到效率较低的实现。

但为什么第一次实施需要效率较低? HashMap' s .containsKey()看起来像这样:

public boolean containsKey(Object key) {
  return getEntry(key) != null;
}

和番石榴的ImmutableMap.containsKey()类似的是:

public boolean containsKey(@Nullable Object key) {
  return get(key) != null;
}

由于这些调用完成了.get()的所有工作,缓存此调用结果的缺点是什么,然后将相同键的后续调用短路到.get() ?看起来成本只是一个指针,但好处意味着"正确"实现这种模式的方法也是"高效的"这样做的方法。


private transient Entry<K,V> lastContainsKeyResult = null;

public boolean containsKey(Object key) {
  lastContainsKeyResult = getEntry(key);
  return lastContainsKeyResult != null;
}

public V get(Object key) {
  if(key != null && lastContainsKeyResult != null && 
     key.equals(lastContainsKeyResult.getKey()) {
    return lastContainsKeyResult.getValue();
  }
  // normal hash lookup
}

4 个答案:

答案 0 :(得分:6)

因为缓存假设一个特定的用例,但实际上会减慢其他用户的速度。它也增加了很多复杂性。

你如何缓存价值?当多个线程一次读取时会发生什么?

坐下来开始思考所有可能发生的各种边缘情况和问题。例如,如果在包含调用和get调用之间更改了值。这种看似简单的变化实际上引入了很多复杂性并且减慢了许多操作,这些操作实际上比这个特定序列更频繁地使用。

您还应该考虑在非缓存之上构建“缓存映射”,但相反的情况是不可能的。

答案 1 :(得分:2)

缓存在某些情况下是有帮助的,在其他情况下是有害的。在基本映射实现中实现缓存会在缓存无用的情况下引起问题。

请记住,可以轻松地围绕非缓存地图构建一个包装器,该地图可根据特定方案进行缓存。

答案 2 :(得分:2)

我认为这不值得:

  • 通常,你根本就不在乎。
  • 你的简单缓存根本不是微不足道的,因为它需要处理修改和并发。
  • 在性能关键代码中,您可以编写丑陋且快速的解决方法并避免开销。
  • 在其他性能关键代码中,您可能需要在没有以下contains的情况下调用get,并且您的缓存会降低速度。

您可以使用始终正确的此代码段。

Value result = map.get(key);
if (result == null && !map.containsKey(key)) {
    // handle absent key
}

除非key不存在或映射到null,否则它只使用一个操作。我想,在你的使用案例中,这并不经常发生。

答案 3 :(得分:1)

其他答案涵盖了要点,但我想特别提到这一点:

  

这第二个实现有它自己的问题。除了不那么简洁和可读之外,它可能是不正确的,因为它无法区分密钥不存在的情况和密钥存在的情况,但显式地映射为null。

我从this回答(this评论中)带走的内容如下:您是否真的想要区分 null 和缺席值?

虽然我不能用一般性的说法,但我会说根据我的个人经验,我从来不需要将键明确地映射到null。

我推测,将null插入到地图中的设计主要用于表示发生了特殊/负面情景。在这种情况下,我可能会考虑使用空对象模式,而不是存储一个实际对象,该对象通过其方法返回值向调用者指示已发生特殊情况。