为何Hashtable读取值呈自然顺序逆序?HashMap与Hashtable读取行为为何不同?
问题解答:Hashtable遍历顺序与HashMap的行为差异
嘿,我来帮你拆解这两个问题——本质都是Hashtable和HashMap的底层实现差异在作祟,咱们一个个说:
问题1:Hashtable读取值呈现“自然顺序逆序”的原因
首先得敲黑板:Hashtable从设计上就不保证任何遍历顺序,你看到的“自然顺序逆序”只是特定测试用例下的巧合,不是它的特性。
拿你的测试键(4、2、3、8)来说,Integer的hashCode就是它本身的值,Hashtable初始容量是11,它计算数组索引的逻辑是:
int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; // 初始tab.length是11
算出来每个键的索引是:4→4,2→2,3→3,8→8。那为啥遍历会像逆序?其实这只是因为Hashtable的迭代器是按数组索引从0到10挨个扫,而你的键对应的索引刚好是2、3、4、8,遍历出来的顺序是2、3、4、8——和你插入的4、2、3、8顺序不一样,看起来像是“逆序”,但换几个键(比如加个5、9),你会发现顺序又乱了。
说白了,Hashtable的遍历顺序完全由哈希值映射后的数组索引决定,没有任何有序性承诺,所谓的“逆序”只是碰巧而已。
问题2:HashMap与Hashtable读取值行为差异的原因
虽然二者都用Integer的hashCode,但它们在好几个关键地方的实现都不一样,导致遍历顺序天差地别:
1. 哈希计算逻辑不同
- Hashtable:直接用键的
hashCode(),然后通过取模运算得到数组索引(还把hash转成正数避免负索引):int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; - HashMap:对键的
hashCode()做了一次扰动处理(把hash的高位和低位做异或),再用位运算得到索引(因为HashMap的容量是2的幂,位运算比取模快多了):
扰动处理是为了让哈希值的高位也参与索引计算,减少碰撞的概率。static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } int index = hash & (tab.length - 1);
2. 数组容量策略不一样
- Hashtable:初始容量是11(质数),扩容时变成
2*旧容量+1(还是质数),用质数当容量是为了降低哈希碰撞的概率。 - HashMap:初始容量是16(2的幂),扩容直接翻倍(一直保持2的幂),这样才能用位运算替代取模,提升性能。
3. 链表插入方式不同
- Hashtable:碰到哈希碰撞时,新的键值对会插在链表的头部,这会打乱同一桶内元素的遍历顺序。
- HashMap:JDK8之前也是头插,但JDK8之后改成了尾插,而且当链表长度超过8时会自动转成红黑树,既优化了查找也让遍历顺序更稳定(相对而言)。
4. 迭代器的遍历逻辑差异
两者的迭代器都是扫内部数组,但因为数组容量、哈希计算的不同,同一个键在两个集合里的数组索引可能完全不一样,再加上链表插入方式的区别,最终遍历出来的顺序自然就差很多了。
划重点
不管是Hashtable还是普通HashMap,都不保证遍历顺序和插入顺序、自然顺序一致。如果需要有序的映射,直接用LinkedHashMap(保持插入顺序)或者TreeMap(按自然顺序排序)就好。
内容的提问来源于stack exchange,提问作者SEY_91
相关产品推荐
相关产品推荐

