今天看到有网友说HashMap存值是按照键的顺序排好的,所以很奇怪为什么说它是无序的,然后下边网友给出了插入很多数据的例子,结果顺序是乱的。首先HashMap无序是指它并没有按照插入的顺序排列,而不是指按照自然顺序排列,其次借这次机会学习一下HashMap的存值原理,为何插入的数据多了顺序就乱了。
首先插入几个比较简单的键:
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("a", 1);
map.put("c", 2);
map.put("b", 3);
map.put("d", 4);
System.out.println(map);
}
输出结果:
{a=1, b=3, c=2, d=4}
果然是排好序的,然后看一下HashMap存值的源码,看一下不存在hash冲突时的存值方式:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
...
}
...
}
可以看出来存值的时候传入了一个计算好的hash值,然后判断map中下标 (n - 1) & hash 处是否有数据,如果无数据则将插入的键和值存入该下标处。那为什么它是排好序的呢?我们来看一下源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
从HashMap的put方法可以看出,传入的hash值是通过hash( )这个方法根据键的hash值计算出的一个值,所以我们插入"a","c","b","d",但它却是排好序的,因为它是根据键的hash值来判断插入的位置的。
接下来试着插入如下几个键值对,同时我们用HashMap中的hash( )方法计算出键的hash值将其输出,并输出插入的下标,因为HashMap的默认大小是16,所以这里n-1直接取15:
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("a", 1);
map.put("b", 3);
map.put("ac", 2);
map.put("aa", 4);
for(Map.Entry entry: map.entrySet()) {
int hash = hash(entry.getKey());
System.out.println(entry.getKey()+"="+entry.getValue()+",hash="+hash+",插入的下标为:"+(15 & hash));
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
输出结果:
aa=4,hash=3104,插入的下标为:0
a=1,hash=97,插入的下标为:1
b=3,hash=98,插入的下标为:2
ac=2,hash=3106,插入的下标为:2
在这里根据hash( )计算出的值与15进行&(与运算)后计算出插入的下标,可以看出,计算后的下标并不按照hash值的大小的顺序,所以就会出现网友说的,多插入一些数据试试,顺序就会乱,这里不再赘述&(与运算)。
但这里 "b" 和 "ac" 下标都是2,它是如何存值的呢?这里就涉及到了HashMap处理hash冲突的方法,看一下存在hash冲突时的存值方式,采用了链表的形式:
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
其中p为下标为(n - 1) & hash处的键值对,如果p的键与插入的内容的hash相等,并且键是相等的,则将p的值更换;
循环向下遍历链表,如果p的下一个节点为空,则将插入的内容放入p的下一个节点,跳出循环结束;
如果在遍历中发现了与插入的内容的hash相等的节点,并且键是相等的,则跳出循环,将该节点的值更换。
据此可以看出,HashMap使用数组加链表的形式实现,当存入的下标相同时,是根据插入的顺序存入相应的链表的,我们将 "b" 和 "ac" 的插入顺序调换后看下输出结果:
aa=4,hash=3104,插入的下标为:0
a=1,hash=97,插入的下标为:1
ac=2,hash=3106,插入的下标为:2
b=3,hash=98,插入的下标为:2
结果验证想法是正确的,当然HashMap中还有resize( )方法调整数组的大小会影响到存值,还有其他一些相关的方法,这里不再深究,后边再学习。