在Java中使用长列表作为映射键

时间:2013-09-23 11:17:48

标签: java list design-patterns collections map

我计划使用一个地图,其中键是相当长的列表(~10 / 100k的小元素):

Map<List<K>, V> myMap = new HashMap<List<K>, V>();

默认的List::hashCode()实现(在AbstractList中)使用循环中所有列表元素的哈希码计算它的哈希码值。此外,List::equals()方法依次比较所有列表元素,并对不同的第一个元素返回false。

除了列表哈希码值没有缓存(JDK 6)并因此每次重新计算,这都会产生意义,这使得这种使用模式效率非常低(地图经常依赖于哈希码) 。 equals()的问题会更少,因为不同的元素在相当低的索引处平均会有第一个不同的项,因此循环会提前中断(但必须比较相同列表的所有元素)。

我正在考虑使用新的自定义KeyList类封装我的列表,将哈希值保留在缓存中以提高性能,但是:

  1. 这不是一件容易的事,因为你必须处理同步问题并实现一些列表接口方法;
  2. 它是侵入性的,因为你必须在客户端代码中使用这个装饰器;
  3. 在比较相同的元素时,这不能解决equals()性能问题。
  4. 处理这种情况会有更好的想法吗?

3 个答案:

答案 0 :(得分:6)

对于这种情况,您的列表必须是 immutable ,否则hashCode()会随着时间而改变,这会破坏哈希映射。如果列表是不可变的,则可以计算hashCode()一次,并将其用作Long对象中包含的密钥。

如果您坚持使用List接口作为密钥,则应实现您提到的KeyList。只需创建一个委托给原始List的List实现,但是重写hashCode()以返回可以在构造函数中初始化的memoized值。

public abstract static class MemoizedHashCodeList<K> implements List<K>{
    private final long hashCode;
    private final List<K> delegate;
    public MemoizedHashCodeList(List<K> delegate) {
        this.delegate = delegate;
        hashCode = delegate.hashCode();
    }

    /* Rest of the List<K> implementation */
}

为了加快实施速度,您可以使用Google Guava的ForwardingList类来为您实现委派模式。

但最重要的是,请确保您的列表是不可变的。不要试图在可变列表上同步破坏你的代码,它只是不起作用。

答案 1 :(得分:2)

我认为您最好的解决方案是KeyList类。我建议将密钥列表的类别隔离到一个方法,并让客户端代码从该方法请求List引用。在其他任何地方,它都可以作为列表引用。

假设密钥列表是不可变的,它们需要用作Map键,您只需要在计算哈希码时进行同步。或者甚至跳过它,并复制String创建其哈希码的方式。

除了标准的哈希码(只有32位)之外,您还可以添加一个具有极低冲突概率的更强大的哈希值,并比较这些哈希值而不是对列表进行逐元素比较。使用您自己的KeyList类可以覆盖equals,以及hashCode。

答案 2 :(得分:0)

我认为您需要重构代码并创建一个单独的对象用作键。你可以做几乎任何事情来创建一个唯一的密钥,也许将列表保存在一个数组中并使用数组索引,或者计算列表的哈希码并使用keep作为密钥。但是将大对象保留为键非常不寻常,我建议你不要这样做。