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


81

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

我的问题是,它可以确保多次迭代的顺序相同吗?例如

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

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

...

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

在上面的代码中,假设映射修改,将迭代在按键组处于相同的顺序。使用Sun的jdk15,它以相同的顺序进行迭代,但是在我依赖此行为之前,我想知道所有JDK是否都将执行相同的操作。

编辑

从答案中我看到我不能依靠它。太糟糕了。我希望能够不必建立一些新的Collection来保证我的订购而摆脱困境。我的代码需要进行迭代,执行一些逻辑,然后以相同的顺序再次进行迭代。我只是从keySet创建一个新的ArrayList来保证顺序。


2
您是否控制从getMap()方法返回哪个Map实现?
Andrey Adamovich,2009年

2
您当然可以在不创建自己的集合的情况下获得一致的排序。参见其他人提到的SortedMap。因此,如果您的getMap()方法改为返回SortedMap,则调用者将知道期望一致的顺序。
PSpeed

我的回答证明了的秩序.keySet().values()一致。不幸的是,公认的答案是错误的。@karoberts-您能看看吗?
Harshal Parekh

Answers:


52

如果API文档中没有明确说明不能保证,则不应依赖它。行为甚至可能从一个JDK版本更改为另一个版本,甚至是同一供应商的JDK。

您可以轻松获得该集合,然后自己对它进行排序,对吗?


3
如其他人所述,如果您可以控制从getMap()返回哪个Map实例,则可以返回SortedMap。在这种情况下,您可能希望从getMap()而不是仅从Map中显式返回SortedMap。
刘坚

4
8.爪哇7和Java之间的HashMap和HashSet的迭代顺序改变
user100464

@KenLiu嗨,我是Java的新手,您能举一个有关如何获取SortedMap的示例吗?非常感谢。
塞西莉亚

您能证明它不一致吗?仅仅因为javadoc没有提到“保证”一词并不意味着它是不一致的。
Parekh

这个答案是不正确的。它们是一致的。我已经在这里证明
Harshal Parekh

58

如果您希望HashMap的迭代顺序不变,则可以使用LinkedHashMap

此外,如果您遍历集合,则应始终使用它。在HashMap的entrySet或keySet上进行迭代比在LinkedHashMap上进行迭代要慢得多。


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。


Set类本身并不能保证其元素没有顺序,只有它是唯一的。因此,当您要求.keySet()时,它将返回Set的实例,该实例没有保证的顺序。如果您要订购,则必须自己进行订购,然后对它们进行排序(通过Collections.sort或SortedSet实现)
Matt

7

Map的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,它打印地图条目:

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] 


2

不一定是。映射的keySet函数返回一个Set,而集合的迭代器方法在其文档中对此进行了说明:

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

因此,除非您使用带有保证的那些类之一,否则没有任何保证。


2

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


1

从逻辑上讲,如果合同上说“不能保证一定的订单”,并且由于“一次发出的订单”是一个特定的订单,那么答案是否定的,那么您就不能两次依靠相同的方式发出订单


1

tl; dr是。


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

证明1:我们向加载HashMap随机密钥和随机值。我们遍历这个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:从这里开始tableNode<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()是一致的。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.