HashMap如何定位桶数组的位置

本文详细探讨了HashMap的工作原理,包括如何通过hash运算确定桶数组位置,以及不同JDK版本下的hash函数差异。HashMap的遍历顺序并不总是插入顺序,但对同一HashMap的多次遍历顺序保持一致。对于期望保持插入顺序,可以使用LinkedHashMap。文章还提供了实例代码进行验证。
摘要由CSDN通过智能技术生成

目录

HashMap描述

HashMap如何定位桶数组的位置

验证

 HashMap遍历顺序

JDK1.7与1.8的hash函数的代码不一样

 推荐博客


HashMap描述

hashmap底层是由数组 + 链表 + 红黑树(当链表的长度大于等于8时,链表会转换为红黑树) 实现,往hashmap中添加键值对的步骤可以分为两步:①通过取模运算得到桶数组的坐标;②往链表或者红黑树中添加节点;

对于第一步的取模运算,是通过hash & (n - 1)实现。因为hashmap的容量大小是2的幂次方,所以可以通过&运算来优化%运算。例如:(17 % 16 )等价于 (17 & (16 - 1))。但是在hashmap获取桶数组坐标的时候,有一个细节需要注意:会先执行hash(key)运算。这个细节很容易被忽视。

HashMap如何定位桶数组的位置

HashMap在通过key,get、put键值对的时候,会先对key调用hash(key)的处理,然后才会是一般的做取模运算(&(n - 1))定位桶数组的位置。最后将键值对添加到链表或者红黑树中。

hash(key)的源码如下:

/**
 * 计算键的 hash 值
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前16位为高位,后16位为低位。hash(key)的含义就是将key的高位与低位做异或运算,其目的是让key的高位也参与到取模运算中,使得键值对分布的更加的均匀。

验证

import java.util.HashMap;

public class HashMapTest {
    public static void main(String[] args) {
        HashMap<Integer, String> hashMap = new HashMap<>(16);
        hashMap.put(0,"");
        hashMap.put(1,"");
        hashMap.put(2,"");
        hashMap.put(3,"");
        hashMap.put(4,"");
        hashMap.put(5,"");

        /* 2的16次方 */
        int high = (int) Math.pow(2,16);

        //为了使得高位不全为0,这里将添加大于2的16次方的整数
        hashMap.put(high + 0, "");
        hashMap.put(high + 1, "");
        hashMap.put(high + 2, "");
        hashMap.put(high + 3, "");
        hashMap.put(high + 4, "");
        hashMap.put(high + 5, "");

        for (Integer key : hashMap.keySet()) {
            System.out.print(key + "->");
        }
    }
}

输出的结果为:

 我们会发现65537(2^{16} + 1)被添加到坐标为0的桶数组链表上,65536(2^{16})被添加到坐标为1的桶数组链表上。这就很奇怪了,因为2^{16} % 16 应该为0;(2^{16} + 1) % 16应该为1。这其中的原因就是因为先对key执行了hash运算,使得高位与低位异或,然后再取模运算导致的。

以key = 65537(2^{16} + 1)为例,整个计算过程如下:

 HashMap遍历顺序

hashmap一般的遍历顺序和插入顺序是不一致的,但是对同一个hashmap的遍历多次顺序会是一致的。 hashmap的遍历顺序是:坐标从小到大,依次遍历每一个桶数组上的链表或者红黑树(红黑树节点会根据插入的顺序,记录自己的前驱和后继)。

若想让map结构的插入顺序与遍历顺序一致,可以使用LinkedHashMap。LinkedHashMap通过维护一个双向链表,使得插入顺序与遍历信息一致。

JDK1.7与1.8的hash函数的代码不一样

1.7使用了hash种子且移位异或运算更加的复杂,但是核心的作用是一样的。

/**
     * Retrieve object hash code and applies a supplemental hash function to the
     * result hash, which defends against poor quality hash functions.  This is
     * critical because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

 推荐博客

HashMap 源码详细分析(JDK1.8) | 田小波的技术博客

LinkedHashMap 源码详细分析(JDK1.8) | 田小波的技术博客

HashMap的底层是由一个哈希数组组成的。而哈希的大小取决于初始化HashMap时指定的初始容量,即bucket数组的大小。比如,如果使用new HashMap(19),那么哈希数组的大小将为32,即2^5。 HashMap在第一次进行put操作时才会分配内存并初始化哈希数组。当put操作需要添加元素时,HashMap会根据元素的哈希值计算它在哈希数组中的位置,并将元素放入对应的位置HashMap在负载因子达到0.75f时会进行扩容,即当哈希数组中的元素个数达到了容量的75%时会自动扩容。扩容过程会重新计算元素在新的哈希中的位置,并将元素重新插入。 当两个对象的哈希值相同时,会发生哈希冲突或碰撞。这意味着它们会被放置在哈希数组的同一个位置上的链表中。在这种情况下,HashMap会从对应位置的链表中遍历查找并比较键的值,以获取正确的值对象。 重新调整HashMap大小时存在一个问题,即重新哈希。因为在扩容时,HashMap需要重新计算元素的哈希值,并将元素插入到新的哈希数组中。这个过程会导致性能的损耗,并且可能会导致哈希冲突的增加。 另外值得一提的是,HashSet的底层也是用HashMap实现的,它只存储key,其中的val都是Object对象。因此,HashSet也具有哈希的特性。您可以通过学习Java中HashMap的源码来深入了解哈希的实现原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值