为什么Sun Java中的HashSet实现使用HashMap作为后台?
查看Java 6的源代码, HashSet<E>
实际上是使用HashMap<E,Object>
,在Set的每个条目上都使用了虚拟对象实例。
我认为这是浪费4个字节(在32位机器上)的大小的条目本身。
但是,为什么仍然使用? 除了维护代码更容易之外,是否有任何理由使用它?
实际上,这不仅仅是HashSet
。 Java 6中Set
接口的所有实现都基于基础Map
。 这不是要求; 这只是实施的方式。 您可以查看Set
的各种实现的文档。
你的主要问题是
但是,为什么仍然使用? 除了维护代码更容易之外,是否有任何理由使用它?
我认为代码维护是一个很大的激励因素。 那么是防止重复和膨胀。
Set
和Map
是类似的接口,因为重复的元素是不允许的。 (我认为唯一不支持Map
Set
是CopyOnWriteArraySet
,这是一个不寻常的集合,因为它是不可变的。)
特别:
从Set
的文档 :
不包含重复元素的集合。 更正式地,集合不包含e1和e2这样的元素,使得e1.equals(e2)和至多一个null元素。 正如其名称所暗示的,这个接口模拟了math集抽象。
Set接口将所有构造函数的合约以及add,equals和hashCode方法的契约之外的其他约束放在超出Collection接口inheritance的约束之外。 其他的inheritance方法的声明也包括在内。 (这些声明附带的规范已经根据Set界面进行了调整,但不包含任何其他规定。)
对构造函数的额外规定并不奇怪,所有构造函数都必须创build一个不包含重复元素(如上所定义)的集合。
从Map
:
将键映射到值的对象。 地图不能包含重复的键; 每个键可以映射到最多一个值。
如果你可以使用现有的代码来实现你的Set
,那么你可以从现有的代码中获得的任何好处(例如速度)都会累积到你的Set
中。
如果您select实现无Map
支持的Set
,则必须复制旨在防止重复元素的代码。 啊,美味的讽刺
也就是说,没有什么能阻止你以不同的方式实现你的Set
。
我猜测,它从来没有成为真正的应用程序或重要基准的重大问题。 为什么复杂的代码没有真正的好处?
还要注意的是,在很多JVM实现中,对象大小都是四舍五入的,所以实际上可能不会增加大小(我不知道这个例子)。 此外, HashMap
的代码很可能被编译和caching。 其他的事情是相同的,更多的代码=>更多的caching未命中=>性能较低。
我的猜测是,HashSet最初是以HashMap的方式实现的,以便快速,轻松地完成。 就代码行而言,HashSet是HashMap的一小部分。
我猜想,它还没有被优化的原因是害怕变化。
但是,浪费比你想象的要糟得多。 在32位和64位上,HashSet比所需要的大4倍,而HashMap比所需要的大2倍。 HashMap可以通过一个数组来实现,其中包含键和值(加上碰撞链)。 这意味着每个条目有两个指针,或者64位虚拟机上有16个字节。 实际上,HashMap在每个条目中都包含一个Entry对象,它为Entry指针添加8个字节,为Entry对象头添加8个字节。 HashSet也使用每个元素32字节,但浪费是4倍而不是2倍,因为它只需要每个元素8字节。
是的你是对的,那里有less量的浪费。 小,因为,对于每个条目,它使用相同的对象PRESENT
(这是宣布的最终)。 因此唯一的浪费就是HashMap中每个条目的值。
大多数情况下,我认为,他们采取这种方法的可维护性和可重用性。 (JCF开发者会认为,我们已经testing了HashMap,为什么不重用呢。)
但是,如果你有巨大的collections,而你是一个记忆怪胎,那么你可以select更好的替代品,如Trove或谷歌collections 。
我看了你的问题,花了一段时间思考你的话。 所以这里是我对HashSet
实现的看法。
有必要让虚拟实例知道该值是否存在于集合中。
看看add方法
public boolean add(E e) { return map.put(e, PRESENT)==null; }
现在让我们来看看放回价值
@返回与键关联的前一个值,如果没有键的映射,则返回null。 (空返回也可以指示映射先前与键关联的是null。)
所以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
)而不是其他一些使用类似Entry的只有4的数据结构,那么是的,我会说他们这样做是因为你提及。
在search这样的页面之后,想知道为什么轻度低效的标准实现,发现com.carrotsearch.hppc.IntOpenHashSet
你的问题:我认为这是浪费4个字节(在32位机器上)的大小的条目本身。
只需要为整个哈希集的数据结构创build一个对象variables,这样可以避免重新编写整个哈希映射types的代码。
private static final Object PRESENT = new Object();
所有的键都有一个值,即PRESENT对象。