HashMap的实现原理

一、概念

    HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

二、特点

1.快速存储

2.快速查找(时间复杂度O(1))

3.可伸缩

三、HashMap数据结构

1、数据结构

        在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针引用(链表),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。 在jdk1.8,hashMap数据结构中加入了红黑树,即链表达到一定的长度就转换为了红黑树。

总结:数组、链表、红黑树(jdk1.8)

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

其中Java源码如下:

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

        可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。(注意jdk1.8之后换成了node数组,可能是链表也可能是树,当前key位置的值超过8个用树存储)

2、HashMap实现存储和读取

1)put存储

public V put (K key, V value)
    //HasnMap允许存放null键和nul值。
    //当key为null时,调用putForNullKxey方法,将value放置在数组第一个位置。
    if (key == null)
        return putForNullKey (value) ;
    //根据key的keyCode 重新计算hash值。
    int hash = hasn(key.hashCode())):
    //搜索指定hash值在对应table中的索引。
    int i = indexFor(hash, tab1e.length)
    //如果i索引处的Entry不为null,通过循环不断遍历 e 元素的下一个元素。
    for (Entry<K,V> e = tab1e[i]; e != null; e = e.next) {
        Object k;
        if(e.hash - hash && ((k = e.key) == key || key.equals(k))) {
            //如果发现己有该键值,则存储新的值,并返回原始值
            V oldVa1be = e.valve;
            e.value = value:
            e. recordAccess (this) ;
            return oldValue;
        }
    }
    //如果i索引处的Entry为null, 表明此处还没有Entry。
    modCount++:
    //将key、value添加到1索引处。
    addEntry (hash, key, value, i):
    return null;
}

根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。通过这种方式就可以高效的解决HashMap的冲突问题。

2)get读取

public V get(Object key) { 
     if (key == null) 
         return getForNullKey(); 
     int hash = hash(key.hashCode()); 
     for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
         e != null; 
         e = e.next) { 
         Object k; 
         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
             return e.value;
     }
     return null;
     }

        从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

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

四、hash算法

        所有的对象都有hashCode(使用key的)

        hash(int h)方法根据key的hashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。

//重新计算哈希值
static final int hash(Object key) {
    int h;
    //(hashCode) ^ (hashCode >>> 16) 
//key如果是null 新hashcode是0 否则 计算新的hashcode
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

        我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率

为什么是16呢?

参考:HashMap中的hash算法总结_晴天-CSDN博客_hashmap的hash算法

三、Hash冲突

Hash冲突:

不同的对象算出来数组下标是相同的

单向链表:用于解决Hash冲突的方案,加入一个next记录下一个节点

四、HashMap的扩容(resize)

触发条件:

数组存储比例达到75% --- 0.75

新建容量翻倍的数组,所有数据重新计算哈希值,放入数组

        当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

       那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过 数组大小*loadFactor 时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了性能的问题,也避免了resize的问题。

五、jdk1.8引入红黑树

红黑树:

一种二叉树,高效的检索效率

触发条件

在链表长度大于8的时候,将链表转为红黑树

链表长度小于8的时候,转回链表

六、HashMap是线程不安全的

        HashMap在多线程put后可能导致get无限循环;多线程put的时候可能导致元素丢失(有空就测一下)

如何使用线程安全的哈希表结构呢,这里列出了几条建议:

        使用Hashtable 类,Hashtable 是线程安全的;

        使用并发包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全;

        或者使用synchronizedMap() 同步方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值