HashMap的实现原理

概述

HashMap是Java中对Map接口的实现类。是其最常用的实现类中之一。主要有以下几个特性

  • HashMap中的key和value都允许为null,但最多只能拥有一个null的key,再次加入null的key会覆盖上一次的key
  • HashMap不保证顺序。
  • HashMap非线程安全。

HashMap 包含如下几个构造器:

  • HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
  • ashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
  • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

HashMap的数据结构

HashMap(jdk1.7)内部是以数组+链表的方式储存的数据。可以参考下图:

在这里插入图片描述

从上图中可以看出,HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个 HashMap 的时候,就会初始化一个数组。

我们通过 JDK 中的 HashMap 源码进行一些学习,首先看一下构造函数:

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
}

我们着重看一下table = new Entry[capacity];。这不就是 Java 中数组的创建方式吗?也就是说在构造函数中,其创建了一个 Entry 的数组,其大小为 capacity(HashMap默认容量16),那么 Entry 又是什么结构呢?看一下源码:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;
    ……
}

我们目前还是只着重核心的部分,Entry 是一个 static class,其中包含了 key 和 value,也就是键值对,另外还包含了一个 next 的 Entry 指针。我们可以总结出:Entry 就是数组中的元素,每个 Entry 其实就是一个 key-value 对,它持有一个指向下一个元素的引用,这就构成了链表。

HashMap(jdk1.8)开始引入红黑树,HashMap底层变成了数组+链表+红黑树实现

在这里插入图片描述

put()方法

put方法被调用时,HashMap会根据key计算出对应的hashcode,然后根据hashcode确定该Entity应该存放在数组的哪个位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

根据hashcode确定该Entity应该存放在数组的哪个位置(即下标)这种设定有一个问题:实际引用中有可能会发生hash碰撞(即两个数据虽然内容不同,但其hashcode有可能是相同的)!因此,HashMap如果发现hashcode已经存在,则会对key进行euqals对比:

  • equals结果是true,则认为确实是同一个key,然后将新的value覆盖旧的value(此时put方法将会返回旧value值)。
  • equals结果是false,则认为是hash碰撞,此时会将之前的Entity作为新Entity的next,此时形成一个链表,新Entity则处在链表的首位。

get()方法

如果理解了上述的put逻辑,则get方法就很容易理解。主要有以下几个步骤:

  • 根据key计算hashcode,然后得出其数组下标(位置)。
  • 找到数组中对应位置的某一元素。
  • 从头到尾遍历链表的每一个Entity,通过key的equals方法找到对应的Entity。

上述的过程中有一个点未详细说明:如何根据key的hashcode计算出对应的数组坐标呢?
HashMap的内部实现用了一个非常巧妙的方法。HashMap的初始容量被定为16,且每次增长都是2的倍数。这样设计的目的是要保证存入map中的元素尽量分散,尽量避免出现链表中出现链表,这可以有效降低数据查询时的处理速度。
key是这么一步步转化成数组下标的:

第一步:Object Key --> int hash

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

如果key是null,则其hash为0;否则便将 hashcodehashcode的高位异或运算。这是为了尽量避免“低位不变,高位变化”时造成的hash冲突。
第二步:hash --> i
上一步计算出的hash是个长度较长的二进制数字,而通常情况下HashMap的底层数组长度(length)较小,因此如果我们进行 hash % length 计算,则一定能得到一个下标,且相对比较分散。而在源码中使用了性能更高的算法:

i = (length - 1) & hash

这个公式对hash和length进行了按位与运算,等价于取余

这时就能说清楚另外一件事情:为什么底层数组的长度总要是2的n次方呢?
下表是个示例:

h & (table.length-1)hashtable.length-1
8 & (15-1):0100&1110= 0100
9 & (15-1):0101&1110= 0100
8 & (16-1):0100&1111= 0100
9 & (16-1):0101&1111= 0101

可以看到,如果数组长度是2的n次方,那么length-1的二进制表示中,一定所有位都是1,此时取&运算则可以完整保留hash响应位置的二进制数据。相反的,如果数组长度不是2的n次方,则出现hash碰撞的可能性大大提高。

归纳

简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,在根据 equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap是基于hashing的原理实现的。当我们使用put(key, value)方法将对象存储到HashMap中时,首先会对键调用hashCode()方法,计算并返回的hashCode用于找到Map数组的bucket位置来存储Node对象。HashMap使用数组和链表的数据结构,即散列桶,来存储键值对映射。HashMap的工作原理是通过计算键的hashCode来确定存储位置,并使用链表解决哈希冲突。当多个键具有相同的hashCode时,它们会被存储在同一个bucket中的链表中。当我们使用get(key)方法从HashMap中获取对象时,会根据键的hashCode找到对应的bucket,然后遍历链表找到对应的值对象。HashMap的实现基于一个线性数组,即Entry\[\],其中保存了键值对的信息。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [javaHashMap原理](https://blog.csdn.net/songhuanfeng/article/details/93905015)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [HashMap实现原理分析](https://blog.csdn.net/vking_wang/article/details/14166593)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值