为什么Sun Java中的HashSet实现使用HashMap作为其后盾?

时间:2010-02-10 09:06:47

标签: java hashmap hashset

查看Java 6的源代码,HashSet<E>实际上是使用HashMap<E,Object>实现的,在Set的每个条目上使用虚拟对象实例。

我认为对于条目本身的大小,浪费了4个字节(在32位机器上)。

但是,为什么还在使用?有没有理由使用它,除了更容易维护代码?

7 个答案:

答案 0 :(得分:19)

实际上,它不只是HashSet Java 6中Set接口的所有实现都基于底层Map。这不是必要条件;这就是实施的方式。您可以通过查看Set的各种实现的文档来了解自己。

您的主要问题是

  

但是,为什么还在使用?在那儿   任何使用它的理由除了制作它   更容易维护代码?

我认为代码维护是一个很大的激励因素。因此,防止重复和膨胀。

SetMap是类似的接口,因为不允许重复的元素。 (我认为Set支持的唯一Map 不是CopyOnWriteArraySet,这是一个不常见的集合,因为它是不可变的。)

具体做法是:

来自documentation of Set

  

包含否的集合   重复元素。更正式的,   集合不包含元素对e1   和e2使得e1.equals(e2)和at   大多数一个null元素。正如所暗示的那样   它的名字,这个界面模型   数学集抽象。

     

Set界面增加了额外的功能   除了继承的规定之外的规定   来自Collection界面,关于   所有建设者的合同和   add,equals和。的合同   hashCode方法。声明   其他继承的方法也是   这里包括为方便起见。 (该   伴随这些的规格   声明已经适应了   设置界面,但它们不包含   任何其他规定。)

     

附加规定   毫不奇怪,构造函数是   所有构造函数都必须创建一个   设置不包含重复   元素(如上所定义)。

来自Map

  

将键映射到值的对象。   地图不能包含重复的键;每个键最多可以映射一个值。

如果您可以使用现有代码实施Set,那么您可以通过现有代码实现的任何好处(例如速度)也会累积到Set

如果您选择在没有Set支持的情况下实施Map,则必须复制旨在防止重复元素的代码。啊,好吃的讽刺。

也就是说,没有什么能阻止您以不同的方式实施Set

答案 1 :(得分:4)

我猜它从未成为真正的应用程序或重要基准的重大问题。为什么复杂的代码没有真正的好处?

另请注意,对象大小在许多JVM实现中都是向上舍入的,因此实际上可能没有增加大小(我不知道这个例子)。此外,HashMap的代码可能会在缓存中编译。其他条件相同,更多代码=&gt;更多缓存未命中=&gt;性能较低。

答案 2 :(得分:4)

我的猜测是HashSet最初是根据HashMap实现的,以便快速轻松地完成。就代码行而言,HashSet是HashMap的一小部分。

我猜它还没有被优化的原因是害怕改变。

然而,浪费比你想象的要糟糕得多。在32位和64位上,HashSet比必要的大4倍,而HashMap比必要的大2倍。可以使用包含键和值的数组(以及用于冲突的链)来实现HashMap。这意味着每个条目有两个指针,或64位VM上的16个字节。实际上,HashMap每个条目包含一个Entry对象,它为Entry的指针增加了8个字节,为Entry对象头添加了8个字节。 HashSet每个元素也使用32个字节,但浪费是4x而不是2x,因为每个元素只需要8个字节。

答案 3 :(得分:3)

是的,你是对的,那里有少量的浪费。小,因为,对于每个条目,它使用相同的对象PRESENT(声明为final)。因此,唯一的浪费是HashMap中每个条目的值。

大多数情况下,我认为,他们采用这种方法来实现可维护性和可重用性。 (JCF开发人员会想到,无论如何我们已经测试了HashMap,为什么不重用它。)

但是如果你有大量的收藏,并且你是一个记忆狂,那么你可以选择更好的替代方案,如TroveGoogle Collections

答案 4 :(得分:3)

我看了你的问题,我花了一些时间思考你所说的话。所以这是我对HashSet实施的看法。

必须让虚拟实例知道集合中是否存在该值。

看一下add方法

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

Abd现在让我们来看看put返回值

  

@returns与key关联的前一个值,如果没有key的映射,则返回null。 (null返回也可以指示映射先前将null与key关联。)

因此PRESENT对象仅用于表示该集合包含e值。我想您问为什么不使用null代替PRESENT。但是,您无法区分该条目之前是否在地图上,因为map.put(key,value)将始终返回null,您无法知道该密钥是否存在。


话虽如此,你可以说他们可以使用像这样的实现

   public boolean add(E e) {

        if( map.containsKey(e) ) {
            return false;
        }

        map.put(e, null);

        return true;

}

我猜他们浪费了4个字节来避免计算hashCode,因为它可能很贵,密钥的两倍(如果要添加密钥)。


如果你提问为什么他们使用HashMap会浪费8个字节(因为Map.Entry)而不是使用类似条目只有4的其他数据结构,那么是的,我会说他们是出于你提到的原因而做的。

答案 5 :(得分:0)

在搜索了这样的页面后,想知道为什么标准实现效率低下,找到了com.carrotsearch.hppc.IntOpenHashSet

答案 6 :(得分:-3)

你的问题: 我认为这对于条目本身的大小来说浪费了4个字节(在32位机器上)。

只为hashset的整个数据结构创建一个Object变量,这样做可以避免再次重写整个hashMap类型的代码。

private static final Object PRESENT = new Object();

所有键都有一个值,即PRESENT对象。