六年前,我烧了几天试图追捕我完全确定的框架随机响应的地方。在精心追逐整个框架确保它全部使用相同的Random实例后,我继续追逐单步执行代码。这是高度重复的迭代自调用代码。更糟糕的是,该死的效果只会在完成大量迭代后出现。在+6小时后,当我在javadoc中为HashSet.iterator()发现一行时,我终于处于智慧状态,表明它不能保证返回元素的顺序。然后我浏览了整个代码库,并用LinkedHashSet替换了所有HashSet实例。而且,我的框架正好向确定性生活迈进!哎呀!
我现在刚刚经历过同样的FREAKIN影响(至少这次只有3个小时)。无论出于何种原因,我都错过了HashMap恰好为其keySet()提供相同方式的小细节。
这是关于这个主题的SO线程,虽然讨论从未完全回答我的问题:Iteration order of HashSet
所以,我很好奇为什么会这样。鉴于这两次我都有一个巨大的单线程java应用程序在完全相同的实例化/插入空间中爬行,在同一台计算机上运行完全相同的JVM参数(来自同一批处理文件的多次运行),几乎没有任何其他运行,可能会扰乱JVM使得HashSet和HashMap在经过大量迭代之后会出现不可预测的行为(并不是因为javadoc说不依赖于顺序而不一致)?
关于这个的任何想法来自源代码(java.util中这些类的实现)或您对JVM的了解(可能某些GC会影响内部java类在分配内部存储空间时获得非归零内存的位置)?
答案 0 :(得分:9)
有一个权衡。如果您希望对元素进行分摊的常量时间 O(1),则迄今为止的技术依赖于像散列这样的随机方案。如果您想要对元素进行有序访问,那么最佳工程权衡只能为您提供 O(ln(n))性能。对于你的情况,也许这并不重要,但是即使相对较小的结构,恒定时间和对数时间之间的差异也会产生很大的差异。
所以是的,您可以仔细查看代码并仔细检查,但它归结为一个相当实际的理论事实。现在是时候刷掉Cormen(或Googly Bookiness here)的副本,这个副本支撑着你家的基础下垂的角落,看看第11章(哈希表)和第13章(红黑树)。这些将分别填充JDK的HashMap和TreeMap实现。
您不希望Map
或Set
返回键/成员的有序列表。这不是他们想要的。地图和集合结构不像基础数学概念那样排序,它们提供不同的性能。这些数据结构的目标(如@thejh所指出的)是有效的摊销insert
,contains
和get
时间,而不是维持排序。您可以了解如何维护散列数据结构以了解权衡取舍。看看Hash Functions和Hash Tables上的维基百科条目(具有讽刺意味的是,注意“无序地图”的Wiki条目重定向到后者)或计算机科学/数据结构文本。
请记住:除非您仔细查看合同是什么,否则不要依赖于ADT(特别是集合)的属性,例如排序,不变性,线程安全或其他任何内容。请注意,对于Map,Javadoc清楚地说:
地图的顺序定义为 顺序上的迭代器 map的集合视图返回它们 元素。一些地图实现, 像TreeMap类一样,具体化 保证他们的秩序;其他, 像HashMap类一样,不要。
Set.iterator()
有类似的内容:
返回元素上的迭代器 在这一套。返回元素 没有特别的顺序(除非这个 set是某个类的实例 提供保证)。
如果您想要这些视图的有序视图,请使用以下方法之一:
Set
,也许你真的想要一个SortedSet
,例如TreeSet
TreeMap
,它允许自然排序键或通过Comparator
SortedSet
个密钥和Map
,它们会表现得更好在摊销时间。Map.keySet()
(或只是您感兴趣的Set
)并将其放入SortedSet
,例如TreeSet
,使用自然排序或具体Comparator
。Map.entrySet().iterator()
对Map.Entry<K,V>
进行迭代。例如。 for (final Map.Entry<K,V> entry : new TreeSet(map.entrySet())) { }
有效访问密钥和值。Arrays.sort()
,它具有不同的性能配置文件(空间和时间)。如果您想查看j.u.HashSet和j.u.HashMap的来源,可以在GrepCode上找到它们。请注意,HashSet只是HashMap的糖。为什么不总是使用排序版本?好吧,正如我在上面提到的那样,性能不同而且在某些应用中很重要。请参阅related SO question here。您还可以看到一些具体的性能数字at the bottom here(我没有仔细查看以确认这些是准确的,但它们恰好证实了我的观点,所以我会轻易地传递链接。: - )
答案 1 :(得分:4)
我之前已经解决了这个问题,订单不是重要的,但确实影响了结果。
Java的多线程特性意味着具有完全相同输入的重复运行可能受到(例如)分配新内存块需要多长时间的微小时间差异的影响,这可能有时需要分页到磁盘以前的内容,以及其他不需要的内容。其他一些不使用该页面的线程可能会继续进行,并且当考虑系统对象时,最终可能会产生不同的对象创建顺序。
这可能会影响JVM的不同运行中等效对象的Object.hashCode()
结果。
对我来说,我决定添加使用LinkedHashMap
的小额开销,以便能够重现我正在运行的测试结果。
答案 2 :(得分:3)
http://download.oracle.com/javase/1.4.2/docs/api/java/lang/Object.html#hashCode()说:
尽可能合理, class定义的hashCode方法 对象确实返回不同的整数 对于不同的对象。 (这是 通常通过转换实现 对象的内部地址 变成一个整数,但是这个 实施技术不是 JavaTM编程所要求的 语言。)
那么内部地址可能会改变吗?
这也意味着您可以通过为应该充当密钥的所有内容编写自己的hashCode()
方法,在不放弃速度的情况下进行修复。
答案 3 :(得分:1)
你永远不应该依赖哈希映射的顺序。
如果你想要一个确定性排序的Map,我建议你使用像TreeMap / TreeSet这样的SortedMap / SortedSet,或者使用LinkedHashMap / LinkedHashSet。我经常使用后者,不是因为程序需要排序,而是因为它更容易读取日志/调试地图的状态。即,当你添加一个密钥时,它每次都会结束。
您可以使用相同的元素创建两个HashMap / HashSet,但根据集合的容量获取不同的顺序。您的代码运行方式可能会出现细微的差异,从而触发不同的最终存储桶大小,从而产生不同的顺序。
e.g。
public static void main(String... args) throws IOException {
printInts(new HashSet<Integer>(8,2));
printInts(new HashSet<Integer>(16,1));
printInts(new HashSet<Integer>(32,1));
printInts(new HashSet<Integer>(64,1));
}
private static void printInts(HashSet<Integer> integers) {
integers.addAll(Arrays.asList(0,10,20,30,40,50,60,70,80,90,100));
System.out.println(integers);
}
打印
[0, 50, 100, 70, 40, 10, 80, 20, 90, 60, 30]
[0, 50, 100, 70, 80, 20, 40, 10, 90, 60, 30]
[0, 100, 70, 40, 10, 50, 80, 20, 90, 60, 30]
[0, 70, 10, 80, 20, 90, 30, 100, 40, 50, 60]
这里有HashSet,它们以相同的顺序添加相同的值,导致迭代器顺序不同。您可能没有使用构造函数,但您的应用程序可能间接导致不同的存储桶大小。