Java HashMap keySet() 迭代顺序是不是一致?
Posted
技术标签:
【中文标题】Java HashMap keySet() 迭代顺序是不是一致?【英文标题】:is the Java HashMap keySet() iteration order consistent?Java HashMap keySet() 迭代顺序是否一致? 【发布时间】:2010-12-25 08:41:34 【问题描述】:我了解从 Map 的 keySet() 方法返回的 Set 不保证任何特定顺序。
我的问题是,它是否保证多次迭代的相同顺序。例如
Map<K,V> map = getMap();
for( K k : map.keySet() )
...
for( K k : map.keySet() )
在上面的代码中,假设地图没有修改,keySets的迭代是否会以相同的顺序进行。使用 Sun 的 jdk15 它确实以相同的顺序迭代,但在我依赖这种行为之前,我想知道是否所有 JDK 都会这样做。
编辑
我从答案中看到我不能依赖它。太糟糕了。我希望不必建立一些新的收藏来保证我的订购。我的代码需要迭代,执行一些逻辑,然后以相同的顺序再次迭代。我将从 keySet 创建一个新的 ArrayList 来保证顺序。
【问题讨论】:
您是否控制从 getMap() 方法返回的 Map 实现? 您当然可以在不创建自己的集合的情况下获得一致的排序。正如其他人所提到的,请参阅 SortedMap。因此,如果您的 getMap() 方法改为返回 SortedMap,调用者将知道需要一致的顺序。 我的answer证明.keySet()
和.values()
的顺序是一致的。不幸的是,接受的答案是错误的。 @karoberts - 你能看一下吗?
【参考方案1】:
tl;dr 是的。
我相信.keySet()
和.values()
的迭代顺序是一致的(Java
8).
证明 1:我们使用随机键和随机值加载 HashMap
。我们使用.keySet()
迭代这个HashMap
并将键和它的对应值加载到LinkedHashMap
(它将保留键和值插入的顺序)。然后我们比较两个地图的.keySet()
和两个地图的.values()
。 结果总是一样的,永远不会失败。
public class Sample3
static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
static SecureRandom rnd = new SecureRandom();
// from here: https://***.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:从 here 开始,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:this answer 还明确指出 - 所以,是的,keySet()、values() 和 entrySet() 按照内部链表使用的顺序返回值。
因此.keySet()
和.values()
是一致的。
【讨论】:
【参考方案2】:您还可以存储 keySet() 方法返回的 Set 实例,并且可以在需要相同订单时使用此实例。
【讨论】:
that 有保证吗?或者每个对iterator()
的调用是否会返回一个具有不同迭代顺序的迭代器,即使是在同一个集合上?【参考方案3】:
我同意 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]
【讨论】:
【参考方案4】:如果 API 文档中没有声明可以保证,那么您不应该依赖它。这种行为甚至可能会从一个版本的 JDK 更改为下一个版本,甚至是来自同一供应商的 JDK。
你可以很容易地得到集合,然后自己排序,对吧?
【讨论】:
正如其他人提到的,如果您可以控制从getMap()返回哪个Map实例,那么您可以返回一个SortedMap。在这种情况下,您可能希望从 getMap() 显式返回 SortedMap 而不仅仅是 Map。 Java 7 和 Java 8 之间的 HashMap 和 HashSet 迭代顺序发生了变化。 @KenLiu 嗨,我对 Java 很陌生,你能给我举一个关于如何获取 SortedMap 的例子吗?非常感谢。 你能证明不一致吗?仅仅因为javadoc没有提到“保证”这个词并不意味着它是不一致的。 这个答案不正确。它们是一致的。我已经证明了here。【参考方案5】:如果你想要一个迭代顺序不变的HashMap,你可以使用LinkedHashMap。
此外,如果您遍历集合,您应该始终使用它。遍历 HashMap 的 entrySet 或 keySet 比遍历 LinkedHashMap 慢得多。
【讨论】:
【参考方案6】:只是为了好玩,我决定编写一些代码,您可以使用它们来保证每次随机顺序。这很有用,因此您可以根据订单捕获您不应该依赖的情况。如果你想依赖顺序,就像其他人所说的那样,你应该使用 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();
【讨论】:
【参考方案7】:Map 只是一个接口(而不是一个类),这意味着实现它的底层类(并且有很多)可能会有不同的行为,API 中 keySet() 的约定并不表示一致的迭代是必须的。
如果您正在查看实现 Map(HashMap、LinkedHashMap、TreeMap 等)的特定类,那么您可以通过查看源代码来了解它是如何实现 keySet() 函数来确定行为的,您d 必须仔细查看算法以查看您要查找的属性是否保留(即,当映射在迭代之间没有任何插入/删除时,迭代顺序一致)。例如,HashMap 的来源在这里(打开 JDK 6):http://www.docjar.com/html/api/java/util/HashMap.java.html
从一个 JDK 到下一个,它可能会有很大差异,所以我绝对不会依赖它。
话虽如此,如果您确实需要一致的迭代顺序,您可能想尝试使用 LinkedHashMap。
【讨论】:
Set 类本身保证它的元素没有顺序,只保证它是唯一的。因此,当您请求 .keySet() 时,它会返回一个没有保证顺序的 Set 实例。如果你想要排序,你必须自己做,并对它们进行排序(使用 Collections.sort 或 SortedSet 实现)【参考方案8】:Map 是一个接口,它没有在文档中定义顺序应该相同。这意味着您不能依赖订单。但是,如果您控制 getMap() 返回的 Map 实现,那么您可以使用 LinkedHashMap 或 TreeMap 并在您遍历它们时始终获得相同顺序的键/值。
【讨论】:
【参考方案9】:不必如此。地图的 keySet 函数返回一个 Set 并且该集合的迭代器方法在其文档中说明了这一点:
“返回此集合中元素的迭代器。元素按特定顺序返回(除非此集合是某个提供保证的类的实例)。”
因此,除非您使用其中一个有保证的类,否则没有。
【讨论】:
【参考方案10】:Hashmap 不保证地图的顺序会随着时间的推移保持不变。
【讨论】:
【参考方案11】:从逻辑上讲,如果合同上说“不保证特定的订单”,并且由于“它出来的订单一次”是特定的订单,那么答案是否定的,你不能取决于它以相同的方式出现两次。
【讨论】:
【参考方案12】:Map 的 API 不保证 任何 顺序,即使在同一对象上多次调用该方法时也是如此。
实际上,如果多次后续调用的迭代顺序发生变化(假设地图本身在两者之间没有变化),我会感到非常惊讶 - 但你不应该(并且根据 API 不能)依赖这个。
编辑 - 如果您希望迭代顺序保持一致,那么您需要一个 SortedMap 来提供这些保证。
【讨论】:
比我快五秒,所以我只会补充一点,即使你可以依赖它,你是否应该这样做也是值得怀疑的。我想知道为什么人们需要依赖它,因为它看起来非常脆弱。以上是关于Java HashMap keySet() 迭代顺序是不是一致?的主要内容,如果未能解决你的问题,请参考以下文章
是啥导致 java.util.HashSet 和 HashMap.keySet() 类的 iterator() 排序稍微不可预测?