簡體   English   中英

是什么導致java.util.HashSet和HashMap.keySet()類的iterator()稍微不可預測的排序?

[英]What causes the slightly unpredictable ordering of the iterator() for the java.util.HashSet and HashMap.keySet() classes?

六年前,我燒了幾天試圖追捕我完全確定的框架隨機響應的地方。 在精心追逐整個框架確保它全部使用相同的Random實例后,我繼續追逐單步執行代碼。 這是高度重復的迭代自調用代碼。 更糟糕的是,該死的效果只會在完成大量迭代后出現。 在+6小時后,當我在javadoc中為HashSet.iterator()發現一行時,我終於處於智慧狀態,表明它不能保證返回元素的順序。 然后我瀏覽了整個代碼庫,並用LinkedHashSet替換了所有HashSet實例。 而且,我的框架正好向確定性生活邁進! 哎呀!

我現在剛剛經歷過同樣的FREAKIN影響(至少這次只有3個小時)。 無論出於何種原因,我都錯過了HashMap碰巧為其keySet()獲得相同方式的細節。

這是關於這個主題的SO線程,雖然討論從來沒有完全回答我的問題: HashSet的迭代順序

所以,我很好奇為什么會這樣。 鑒於我兩次都有一個巨大的單線程java應用程序在完全相同的實例化/插入空間中使用完全相同的JVM參數(來自同一批處理文件的多次運行)在同一台計算機上運行,​​幾乎沒有其他任何運行,可能會擾亂JVM使得HashSet和HashMap在經過大量迭代之后會表現得不可預測(並不是因為javadoc說不依賴於順序而不一致)?

從源代碼(java.util中的這些類的實現)或者你對JVM的了解(可能是某些GC影響內部java類在分配內部存儲空間時獲得非零內存的位置)的任何想法?

簡答

有一個權衡。 如果您希望對元素進行分攤的常量時間O(1)訪問,那么迄今為止的技術依賴於像散列這樣的隨機方案。 如果您想要對元素進行有序訪問,那么最佳工程權衡只能為您提供O(ln(n))性能。 對於你的情況,也許這並不重要,但是即使相對較小的結構,恆定時間和對數時間之間的差異也會產生很大的差異。

所以,是的,您可以仔細查看代碼並仔細檢查,但它歸結為一個相當實際的理論事實。 現在是刷掉那些支撐你房子基礎的下垂角落的Cormen (或Googly Bookiness )副本上的灰塵的好時機,看看第11章(哈希表)和第13章(紅黑樹)。 這些將分別填充JDK的HashMap和TreeMap實現。

答案很長

您不希望MapSet返回鍵/成員的有序列表。 這不是他們想要的。 地圖和集合結構不像基礎數學概念那樣排序,它們提供不同的性能。 這些數據結構的目標(如@thejh所指出的)是有效的攤銷insertcontainsget時間,而不是維持排序。 您可以了解如何維護散列數據結構以了解權衡取舍。 看看關於Hash函數哈希表的Wikipedia條目(具有諷刺意味的是,注意“無序映射”的Wiki條目重定向到后者)或計算機科學/數據結構文本。

請記住:除非您仔細查看合同是什么,否則不要依賴於ADT(特別是集合)的屬性,例如訂購,不變性,線程安全或其他任何內容。 請注意,對於Map,Javadoc清楚地說:

地圖的順序定義為地圖集合視圖上的迭代器返回其元素的順序。 一些地圖實現,比如TreeMap類,對它們的順序做出了特定的保證; 其他人,比如HashMap類,沒有。

Set.iterator()有類似的:

返回此set中元素的迭代器。 元素以無特定順序返回(除非此集合是某個提供保證的類的實例)。

如果您想要這些的有序視圖,請使用以下方法之一:

  • 如果它只是一個Set ,也許你真的想要一個SortedSet比如TreeSet
  • 使用TreeMap ,它允許自然排序鍵或通過Comparator進行特定排序
  • 摘要你的數據結構,如果這是你想要的行為,它可能是一個特定於應用程序的東西,並維護一個SortedSet鍵和一個Map ,它將在攤銷時間內表現更好。
  • 獲取Map.keySet() (或者只是您感興趣的Set )並將其放入SortedSet例如TreeSet ,使用自然順序或特定的Comparator
  • 在對Map.Entry<K,V>進行排序后,使用Map.entrySet().iterator()它。 例如for (final Map.Entry<K,V> entry : new TreeSet(map.entrySet())) { }可以有效地訪問鍵和值。
  • 如果您只是這樣做一次,您可以從結構中獲取一組值並使用Arrays.sort() ,它具有不同的性能配置文件(空間和時間)。

鏈接到源

如果您想查看juHashSetjuHashMap的源代碼,可以在GrepCode上找到它們。 請注意,HashSet只是HashMap的糖。 為什么不總是使用排序版本? 好吧,正如我在上面提到的那樣,性能不同而且在某些應用中很重要。 請在此處查看相關的SO問題 您還可以在底部看到一些具體的性能數字(我沒有仔細查看以確認這些是准確的,但它們恰好證實了我的觀點,所以我會輕松地傳遞鏈接。:-)

我之前已經解決了這個問題,訂單並不重要 ,但確實影響了結果。

Java的多線程特性意味着具有完全相同輸入的重復運行可能受到(例如)分配新內存塊需要多長時間的微小時間差異的影響,這可能有時需要分頁到磁盤內容,以及其他不需要的內容。 其他一些不使用該頁面的線程可能會繼續,並且當考慮系統對象時,您最終可能會創建不同的對象創建順序。

這可能會影響JVM的不同運行中的等效對象的Object.hashCode()結果。

對我來說,我決定添加使用LinkedHashMap的小額開銷,以便能夠重現我正在運行的測試的結果。

http://download.oracle.com/javase/1.4.2/docs/api/java/lang/Object.html#hashCode ()說:

盡可能合理,Object類定義的hashCode方法確實為不同的對象返回不同的整數。 (這通常通過將對象的內部地址轉換為整數來實現,但JavaTM編程語言不需要此實現技術。)

那么內部地址可能會改變嗎?

這也意味着您可以通過為應該充當鍵的所有內容編寫自己的hashCode()方法,在不放棄速度的情況下修復它。

你永遠不應該依賴哈希映射的順序。

如果你想要一個確定性排序的Map,我建議你使用像TreeMap / TreeSet這樣的SortedMap / SortedSet,或者使用LinkedHashMap / LinkedHashSet。 我經常使用后者,不是因為程序需要排序,而是因為它更容易讀取日志/調試地圖的狀態。 即,當你添加一個鍵時,它每次都會結束。

您可以使用相同的元素創建兩個HashMap / HashSet,但根據集合的容量獲取不同的順序。 代碼運行方式的細微差別可能會觸發不同的最終存儲桶大小,從而導致不同的順序。

例如

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,它們以相同的順序添加相同的值,導致不同的迭代器順序。 您可能沒有使用構造函數,但您的應用程序可能會間接導致不同的存儲桶大小。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM