87

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

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

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

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

...

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

在上面的代码中,假设 map没有被修改,那么对 keySet 的迭代是否会按照相同的顺序进行。使用 Sun 的 jdk15 它确实以相同的顺序迭代,但在我依赖这种行为之前,我想知道是否所有 JDK 都会这样做。

编辑

我从答案中看到我不能依赖它。太糟糕了。我希望不必建立一些新的收藏来保证我的订购。我的代码需要迭代,执行一些逻辑,然后以相同的顺序再次迭代。我将从 keySet 创建一个新的 ArrayList 来保证顺序。

4

12 回答 12

63

如果你想要一个迭代顺序不变的 HashMap,你可以使用LinkedHashMap 。

此外,如果您遍历集合,您应该始终使用它。遍历 HashMap 的 entrySet 或 keySet 比遍历 LinkedHashMap 慢得多。

于 2011-08-10T18:48:59.920 回答
56

如果 API 文档中没有说明它是有保证的,那么你不应该依赖它。这种行为甚至可能会从 JDK 的一个版本更改为下一个版本,即使是同一供应商的 JDK。

你可以很容易地得到一套,然后自己排序,对吧?

于 2009-12-10T17:55:38.820 回答
9

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

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

从一个 JDK 到下一个 JDK 可能会有很大差异,所以我绝对不会依赖它。

话虽如此,如果您确实需要一致的迭代顺序,您可能想尝试使用 LinkedHashMap。

于 2009-12-10T18:01:31.040 回答
7

Map 的 API 不保证任何排序,即使在同一对象上多次调用该方法之间也是如此。

在实践中,如果多次后续调用的迭代顺序发生变化(假设地图本身在两者之间没有变化),我会感到非常惊讶 - 但你不应该(并且根据 API 不能)依赖于此。

编辑 - 如果你想依赖迭代顺序是一致的,那么你需要一个提供这些保证的SortedMap 。

于 2009-12-10T17:53:52.207 回答
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();
    }
}
于 2009-12-10T18:19:05.063 回答
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 打印地图条目:

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] 

您可以看到键顺序的差异。键的排序顺序很好,但复制映射的键的顺序再次与早期映射的顺序相同。我不知道这是否有效,但是对于具有相同键的两个哈希图,键的顺序是相同的。这意味着如果此 JVM 版本的 HashMap 实现,由于键插入算法的固有性质,键的顺序不能保证,但对于具有相同键的两个映射可以是相同的。

现在,当我使用 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] 
于 2016-07-24T05:31:52.193 回答
3

Hashmap 不保证地图的顺序会随着时间的推移保持不变。

于 2009-12-10T17:54:57.883 回答
3

tl;博士是的。


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

证明 1HashMap :我们用随机键和随机值加载 a 。我们HashMap使用.keySet()并加载键及其对应的值对此进行迭代LinkedHashMap(它将保留插入的键和值的顺序)。然后我们比较.keySet()两个地图和.values()两个地图的。结果总是一样的,永远不会失败。

public class Sample3 {

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

    // from here: https://stackoverflow.com/a/157202/8430155
    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()是一致的。

于 2020-08-20T23:44:16.063 回答
2

它不一定是。地图的 keySet 函数返回一个 Set 并且该集合的迭代器方法在其文档中说明了这一点:

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

因此,除非您使用其中一个有保证的类,否则没有。

于 2009-12-10T17:55:06.407 回答
2

Map 是一个接口,它没有在文档中定义顺序应该相同。这意味着您不能依赖订单。但是,如果您控制 getMap() 返回的 Map 实现,那么您可以使用 LinkedHashMap 或 TreeMap 并在遍历它们时始终获得相同顺序的键/值。

于 2009-12-10T17:58:18.957 回答
1

从逻辑上讲,如果合同上说“不保证特定的订单”,并且由于“一次出来的订单”是一个特定的订单,那么答案是否定的,你不能指望它以同样的方式出现两次。

于 2009-12-10T17:54:50.447 回答
0

您还可以存储由 keySet() 方法返回的 Set 实例,并且可以在需要相同订单时使用此实例。

于 2017-03-21T06:06:57.257 回答