Java中的HashMap的keySet()迭代顺序是否一致?

93

我了解从Map的keySet()方法返回的Set不保证任何特定的顺序。

我的问题是,它是否在多次迭代中保证相同的顺序。例如:

Map<K,V> map = getMap();

for( K k : map.keySet() )
{
}

...

for( K k : map.keySet() )
{
}
在上述代码中,假设地图没有被修改,那么遍历keySet的顺序是否相同。在使用Sun的jdk15时,它以相同的顺序迭代,但在依赖此行为之前,我想知道所有JDK是否都会执行相同的操作。
编辑:从答案中我看到不能这样依赖它。太糟糕了。我希望不必构建一些新集合来保证我的排序。我的代码需要遍历,执行一些逻辑,然后再次按相同的顺序遍历。我将从keySet创建一个新的ArrayList来保证顺序。

2
你能控制从getMap()方法返回的Map实现吗? - Andrey Adamovich
2
你确实可以在不创建自己的集合的情况下获得一致的排序。如其他人所提到的那样,可以使用SortedMap。因此,如果你的getMap()方法返回SortedMap,调用者将知道可以期望一致的排序。 - PSpeed
我的回答证明了.keySet().values()的顺序是一致的。不幸的是,被接受的答案是错误的。@karoberts - 你能看一下吗? - Harshal Parekh
12个回答

66
你可以使用 LinkedHashMap,如果你想要一个迭代顺序不变的 HashMap 。
此外,如果你需要遍历集合,你应该总是使用 LinkedHashMap。相比于 HashMap 的 entrySet 或 keySet 遍历,LinkedHashMap 更快。

58
如果API文档没有明确保证,那你就不应该依靠它。甚至行为在JDK的一个版本到另一个版本之间,甚至是同一供应商的JDK版本之间都可能会发生改变。
你可以轻松地获取集合,然后自己进行排序,对吧?

3
正如其他人提到的那样,如果你可以控制从getMap()返回的Map实例,那么你可以返回一个SortedMap。在这种情况下,你可能想要明确地从getMap()返回一个SortedMap而不仅仅是一个Map。 - Ken Liu
4
HashMap 和 HashSet 在 Java 7 和 Java 8 中的迭代顺序发生了变化。 - user100464
@KenLiu 你好,我对Java非常陌生,能否给我一个获取SortedMap的例子?非常感谢。 - Cecilia
你能证明它是不一致的吗?仅仅因为Javadoc没有提到“保证”这个词并不意味着它是不一致的。 - Harshal Parekh
这个答案是不正确的。它们是一致的。我已经在这里证明了它。 - Harshal Parekh
@HarshalParekh 这个答案在文档方面是正确的(尽管我不同意对键进行排序一定是一个好主意 - 最好使用 LinkedHashMap)。文档(截至撰写本文时)说:“此类不能保证映射的顺序;特别是,它不能保证顺序随时间的推移保持不变。” - ggorlen

9

Map只是一个接口(而不是类),这意味着实现它的底层类(有很多种)可能会有不同的行为,而API中keySet()的契约并未指示需要一致的迭代。

如果您正在查看实现Map的特定类(HashMap、LinkedHashMap、TreeMap等),那么可以通过查看源代码来确定它如何实现keySet()函数,从而确定行为,您必须仔细查看算法,以确定所需属性是否被保留(即在映射在迭代之间没有插入/删除时的一致迭代顺序)。例如,HashMap的源代码在此处(open JDK 6):http://www.docjar.com/html/api/java/util/HashMap.java.html

它可能因JDK版本而异,因此我肯定不会依赖它。

话虽如此,如果一致的迭代顺序是您真正需要的东西,那么您可能需要尝试使用LinkedHashMap。


Set类本身并不保证它的元素顺序,只保证它是唯一的。所以当你要求.keySet()时,它返回一个没有保证顺序的Set实例。如果你想要顺序,你必须自己动手排序(可以使用Collections.sort或SortedSet实现)。 - Matt

7
地图API不保证任何排序,即使在同一对象上多次调用该方法之间也是如此。
实际上,如果迭代顺序在多个后续调用中更改(假设地图本身没有在其间更改),我会感到非常惊讶 - 但您不应该(根据API不能)依赖于此。
编辑 - 如果您希望依赖于迭代顺序保持一致,则需要SortedMap,它提供了确切的保证。

你比我快了五秒,所以我只想补充一点,即使你可以依赖它,是否应该这样做还是值得怀疑的。我不禁要问为什么有人需要依赖它,因为它似乎非常脆弱。 - PSpeed

5
为了好玩,我决定编写一些代码,可以确保每次都有不同的随机顺序。这很有用,因为你可以捕捉到依赖于顺序但实际上不应该依赖于顺序的情况。如果你想依赖于顺序,那么就像其他人所说的那样,你应该使用SortedMap。如果你只是使用Map并且碰巧依赖于顺序,那么使用以下的RandomIterator就可以捕捉到它。我只会在测试代码中使用它,因为它会占用更多的内存。
你还可以包装Map(或Set),使它们返回RandomeIterator,然后让你使用for-each循环。
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class Main
{
    private Main()
    {
    }

    public static void main(final String[] args)
    {
        final Map<String, String> items;

        items = new HashMap<String, String>();
        items.put("A", "1");
        items.put("B", "2");
        items.put("C", "3");
        items.put("D", "4");
        items.put("E", "5");
        items.put("F", "6");
        items.put("G", "7");

        display(items.keySet().iterator());
        System.out.println("---");

        display(items.keySet().iterator());
        System.out.println("---");

        display(new RandomIterator<String>(items.keySet().iterator()));
        System.out.println("---");

        display(new RandomIterator<String>(items.keySet().iterator()));
        System.out.println("---");
    }

    private static <T> void display(final Iterator<T> iterator)
    {
        while(iterator.hasNext())
        {
            final T item;

            item = iterator.next();
            System.out.println(item);
        }
    }
}

class RandomIterator<T>
    implements Iterator<T>
{
    private final Iterator<T> iterator;

    public RandomIterator(final Iterator<T> i)
    {
        final List<T> items;

        items = new ArrayList<T>();

        while(i.hasNext())
        {
            final T item;

            item = i.next();
            items.add(item);
        }

        Collections.shuffle(items);
        iterator = items.iterator();
    }

    public boolean hasNext()
    {
        return (iterator.hasNext());
    }

    public T next()
    {
        return (iterator.next());
    }

    public void remove()
    {
        iterator.remove();
    }
}

4
我同意LinkedHashMap的观点。在我尝试按键对HashMap排序时,我遇到了问题,以下是我的发现和经验。
我创建HashMap的代码:
HashMap<Integer, String> map;

@Before
public void initData() {
    map = new HashMap<>();

    map.put(55, "John");
    map.put(22, "Apple");
    map.put(66, "Earl");
    map.put(77, "Pearl");
    map.put(12, "George");
    map.put(6, "Rocky");

}

我有一个名为 showMap 的函数,它会打印 map 的条目:
public void showMap (Map<Integer, String> map1) {
    for (Map.Entry<Integer,  String> entry: map1.entrySet()) {
        System.out.println("[Key: "+entry.getKey()+ " , "+"Value: "+entry.getValue() +"] ");

    }

}

现在,当我在排序之前打印地图时,它会打印以下顺序:
Map before sorting : 
[Key: 66 , Value: Earl] 
[Key: 22 , Value: Apple] 
[Key: 6 , Value: Rocky] 
[Key: 55 , Value: John] 
[Key: 12 , Value: George] 
[Key: 77 , Value: Pearl] 

这与映射键放置的顺序基本不同。

现在,当我使用映射键进行排序时:

    List<Map.Entry<Integer, String>> entries = new ArrayList<>(map.entrySet());

    Collections.sort(entries, new Comparator<Entry<Integer, String>>() {

        @Override
        public int compare(Entry<Integer, String> o1, Entry<Integer, String> o2) {

            return o1.getKey().compareTo(o2.getKey());
        }
    });

    HashMap<Integer, String> sortedMap = new LinkedHashMap<>();

    for (Map.Entry<Integer, String> entry : entries) {
        System.out.println("Putting key:"+entry.getKey());
        sortedMap.put(entry.getKey(), entry.getValue());
    }

    System.out.println("Map after sorting:");

    showMap(sortedMap);

输出结果为:

Sorting by keys : 
Putting key:6
Putting key:12
Putting key:22
Putting key:55
Putting key:66
Putting key:77
Map after sorting:
[Key: 66 , Value: Earl] 
[Key: 6 , Value: Rocky] 
[Key: 22 , Value: Apple] 
[Key: 55 , Value: John] 
[Key: 12 , Value: George] 
[Key: 77 , Value: Pearl] 

您可以看到键的顺序有所不同。排序后的键的顺序是可以接受的,但复制地图的键的顺序与早期地图的顺序相同。我不知道是否有效说,但对于具有相同键的两个哈希映射,键的顺序相同。这意味着键的顺序不能保证,但是如果HashMap实现此JVM版本的键插入算法的固有特性相同,则两个具有相同键的映射的键可以相同。
现在,当我使用LinkedHashMap将排序条目复制到HashMap时,我获得了所需的结果(这是自然的,但重点是关于HashMap键的顺序)。
    HashMap<Integer, String> sortedMap = new LinkedHashMap<>();

    for (Map.Entry<Integer, String> entry : entries) {
        System.out.println("Putting key:"+entry.getKey());
        sortedMap.put(entry.getKey(), entry.getValue());
    }

    System.out.println("Map after sorting:");

    showMap(sortedMap);

输出:

Sorting by keys : 
Putting key:6
Putting key:12
Putting key:22
Putting key:55
Putting key:66
Putting key:77
Map after sorting:
[Key: 6 , Value: Rocky] 
[Key: 12 , Value: George] 
[Key: 22 , Value: Apple] 
[Key: 55 , Value: John] 
[Key: 66 , Value: Earl] 
[Key: 77 , Value: Pearl] 

3

Hashmap不能保证映射的顺序会随时间保持不变。


2

这并非必须如此。地图的keySet函数返回一个Set,该集合的迭代器方法在其文档中说明:

“返回此集合中元素的迭代器。元素以任意顺序返回(除非此集合是某个提供保证的类的实例)。”

因此,除非您正在使用具有保证的某些类,否则没有特定的顺序。


2

Map是一个接口,文档中没有规定顺序应该是相同的。这意味着你不能依赖于顺序。但如果你控制getMap()返回的Map实现,那么你可以使用LinkedHashMap或TreeMap,并且每次迭代时获得相同的键/值顺序。


2

简短概述 是的。


我相信在Java 8中,.keySet().values()的迭代顺序是一致的。

证据1:我们使用随机键和随机值加载一个HashMap。我们使用.keySet()在这个HashMap上进行迭代,并将键及其对应的值加载到一个LinkedHashMap中(它会保留插入的键和值的顺序)。然后我们比较两个地图的.keySet().values()它总是相同的,从不失败。

public class Sample3 {

    static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    static SecureRandom rnd = new SecureRandom();

    // from here: https://dev59.com/snVD5IYBdhLWcg3wQJKT#157202
    static String randomString(int len){
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
            sb.append(AB.charAt(rnd.nextInt(AB.length())));
        }
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        for (int j = 0; j < 10; j++) {
            Map<String, String> map = new HashMap<>();
            Map<String, String> linkedMap = new LinkedHashMap<>();

            for (int i = 0; i < 1000; i++) {
                String key = randomString(8);
                String value = randomString(8);
                map.put(key, value);
            }

            for (String k : map.keySet()) {
                linkedMap.put(k, map.get(k));
            }

            if (!(map.keySet().toString().equals(linkedMap.keySet().toString()) &&
                  map.values().toString().equals(linkedMap.values().toString()))) {
                // never fails
                System.out.println("Failed");
                break;
            }
        }
    }
}

证明2: 从这里可以看到,table是一个Node<K,V>类的数组。我们知道,遍历一个数组每次都会得到相同的结果。

/**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
transient Node<K,V>[] table;

负责.values()的类:
final class Values extends AbstractCollection<V> {
    
    // more code here

    public final void forEach(Consumer<? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

负责.keySet()的类:
final class KeySet extends AbstractSet<K> {

    // more code here

    public final void forEach(Consumer<? super K> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

仔细查看两个内部类。它们基本相同,除了:

if (size > 0 && (tab = table) != null) {
    int mc = modCount;
    for (int i = 0; i < tab.length; ++i) {
        for (Node<K,V> e = tab[i]; e != null; e = e.next)
            action.accept(e.key);               <- from KeySet class
            // action.accept(e.value);          <- the only change from Values class
    }
    if (modCount != mc)
        throw new ConcurrentModificationException();
}

他们在同一个数组table上迭代,以支持KeySet类中的.keySet()Values类中的.values()
证明3:这个回答也明确说明 - 所以,是的,keySet()、values()和entrySet()按照内部链接列表使用的顺序返回值。 因此,.keySet().values()保持一致。

1
该规范明确说明没有排序的保证。依赖于实现细节会使您在处理意外输入、部署或升级版本时面临无法解释的故障风险。在这种情况下,API提供了一个明确排序的数据结构,因此即使它不是非常危险,也没有理由这样做。 - ggorlen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接