【问题标题】:Why don't common Map implementations cache the result of Map.containsKey() for Map.get()为什么常见的 Map 实现不为 Map.get() 缓存 Map.containsKey() 的结果
【发布时间】:2014-06-19 08:26:24
【问题描述】:

使用映射的常见模式是检查键是否存在,然后仅在存在时才对值进行操作,请考虑:

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.containsKey() 看起来像这样:

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

而Guava的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
}

【问题讨论】:

  • 这是什么DefaultValue
  • 您必须同步 containsKeyget 方法才能在多线程代码中正常工作。
  • 您还需要添加脏标志以指示地图何时发生更改。
  • @SotiriosDelimanolis 只是一个虚拟对象,它不应该与问题有任何关系......
  • @Duncan 非常好,这将引起关注。

标签: java collections guava short-circuiting


【解决方案1】:

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

如何缓存值?当多个线程同时读取时会发生什么?

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

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

【讨论】:

  • 关于缓存的好点子,在发布问题后,我认为CachedContainsMap 装饰器可能有意义;虽然它也会给代码增加它自己的复杂性和迟钝性。
【解决方案2】:

缓存在某些情况下是有用的,在其他情况下是有害的。在基本地图实现中实现缓存会在缓存无用的情况下导致问题。

请记住,可以轻松地围绕非缓存映射构造一个包装器,该映射会根据特定场景进行缓存。

【讨论】:

    【解决方案3】:

    我觉得不值得:

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

    你可以使用这个永远正确的sn-p。

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

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

    【讨论】:

      【解决方案4】:

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

      第二个实现有它自己的问题。除了不太简洁和可读性之外,它还可能不正确,因为它无法区分键不存在和键存在但显式映射到 null 的情况。

      我从this 答案(this 评论中)带走的东西如下:你真的想区分null 和缺席值吗?

      虽然我不能笼统地说,但我想说,根据我的个人经验,我从来不需要将键显式映射到 null。

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

      【讨论】:

      • 我完全同意,但遗憾的是,除非 Map 的规范被更新,否则实用程序代码等必须考虑这种不幸的可能性。
      • 不确定你的意思; HashMap.get()supports both null keys and values。 Guava 的 ImmutableMap 没有,这太棒了,但我不能依赖代码需要 Map 的地方。
      猜你喜欢
      • 2021-08-17
      • 2014-03-09
      • 2014-05-16
      • 2020-01-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-14
      相关资源
      最近更新 更多