关于HashMap存值的学习

本文深入探讨了HashMap在Java中的存值原理,解释了为何插入数据后看似有序却又在大量数据下变得无序的现象。通过对源码的分析,揭示了HashMap如何利用hash值确定插入位置,并阐述了其处理hash冲突的链表方法。

       今天看到有网友说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( )方法调整数组的大小会影响到存值,还有其他一些相关的方法,这里不再深究,后边再学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值