关于Hashtable与HashMap的keySet()遍历顺序的疑问
问题背景
我最近学习Java时发现,HashMap和Hashtable理论上不会保留插入顺序或键的顺序,但测试中出现了例外情况。
测试代码
import java.util.HashMap; import java.util.Hashtable; public class Main { public static void main(String[] args) { Hashtable<Integer, Integer> hashtableInt = new Hashtable<>(); Hashtable<String, Integer> hashtableStr = new Hashtable<>(); HashMap<Integer, Integer> hashMapInt = new HashMap<>(); HashMap<String, Integer> hashMapStr = new HashMap<>(); for (int i = 0; i < 25; i++) { hashtableInt.put(i, i); hashtableStr.put(String.valueOf(i), i); hashMapInt.put(i, i); hashMapStr.put(String.valueOf(i), i); } System.out.println("Hashtable with Integer keys : "); for (int key : hashtableInt.keySet()) System.out.print(key + " "); System.out.println("\n\nHashtable with String keys : "); for (String key : hashtableStr.keySet()) System.out.print(key + " "); System.out.println("\n\nHashMap with Integer keys : "); for (int key : hashMapInt.keySet()) System.out.print(key + " "); System.out.println("\n\nHashMap with String keys : "); for (String key : hashMapStr.keySet()) System.out.print(key + " "); } }
输出结果
Hashtable with Integer keys : 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 Hashtable with String keys : 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 24 3 23 2 22 1 21 0 20 HashMap with Integer keys : 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 HashMap with String keys : 22 23 24 10 11 12 13 14 15 16 17 18 19 0 1 2 3 4 5 6 7 8 9 20 21
从输出可见:使用Integer作为键的哈希表存在特定顺序,仅使用String作为键的哈希表无固定顺序,且Hashtable与HashMap的Integer键遍历顺序相反。
疑问
- 为什么Hashtable.keySet()和HashMap.keySet()会出现特定顺序?通常哈希表和集合是无特定顺序的,我是否遗漏了什么?
- 为什么Hashtable.keySet()和HashMap.keySet()的顺序相反?两者都是哈希实现,内部机制有何不同?
- 若keySet()存在顺序,为何使用String键时又会出现无规律的情况?
解答
1. Integer键出现特定顺序的原因
哈希表的遍历顺序本质是内部数组的遍历顺序——哈希表会根据键的哈希值计算数组下标,将键值对存入对应槽位,遍历则按数组槽位的顺序进行。
对于Integer类型,它的hashCode()直接返回自身数值:
public int hashCode() { return Integer.hashCode(value); } // Integer.hashCode的实现为return i;
当插入连续的Integer值(0到24)时,它们的哈希值也连续,计算后的数组下标会呈现连续分布。此时遍历数组槽位,就会呈现出类似有序的结果。但要明确:这种顺序是哈希表内部实现的副作用,并非API承诺的行为。JDK官方文档明确说明,HashMap/Hashtable的keySet遍历顺序不固定,也不保证与插入顺序或键的自然顺序一致。一旦JDK版本更新、哈希表扩容策略变化,或插入的Integer值不连续,这种“假顺序”就会消失。
2. Hashtable与HashMap遍历顺序相反的原因
两者的内部实现细节差异导致了顺序相反:
- HashMap:内部数组容量默认是2的幂,哈希下标通过
hash & (length-1)计算,遍历从数组下标0开始到末尾。连续的Integer值对应连续的小下标,因此遍历顺序是0到24。 - Hashtable:内部数组容量默认是11(扩容规则为
2*length+1,始终是奇数),哈希下标通过hash % length计算(哈希值由hashCode() ^ (hashCode() >>> 15)生成),且链表采用头插法。这些细节导致大的Integer值对应的数组下标更靠前,加上遍历逻辑的差异,最终呈现逆序遍历结果。
核心是:两者的哈希函数、扩容策略、链表插入方式均不同,这些内部细节导致了遍历顺序的差异,但都不属于API承诺的行为,只是特定场景下的巧合。
3. String键顺序无规律的原因
String的hashCode()基于字符序列计算,并非简单的自身值:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
不同字符串的哈希值分布无规律,计算后的数组下标会随机分散在数组的各个槽位,遍历自然呈现杂乱无章的顺序,这才是哈希表遍历的正常表现。
内容的提问来源于stack exchange,提问作者CheongJu Not Apple

