深入理解HashMap底层原理

本文深入探讨了HashMap的工作原理,包括其内部结构、哈希算法、快速存取机制、扩容策略及与Hashtable的区别等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.介绍
    对于 HashSet 而言,其采用 Hash 算法决定元素在Set中的存储位置,这样可以保证元素的快速存取;
    对于 HashMap 而言,其将 key-value 当成一个整体(Entry 对象)来处理,其也采用同样的 Hash 算法
    去决定 key-value 的存储位置从而保证键值对的快速存取。虽然 HashMap 和 HashSet 实现的接口规范不同,
    但是它们底层的 Hash 存储机制完全相同。实际上,HashSet 本身就是在 HashMap 的基础上实现的。

2.哈希的概念
  Hash 就是把任意长度的输入(又叫做预映射),通过哈希算法,变换成固定长度的输出(通常是整型),该输出就是哈希值。
       不同的输入可能会散列成相同的输出,从而不可能从散列值来唯一的确定输入值。

3.哈希表的特点
  数组的特点是:寻址容易,插入和删除困难;
       链表的特点是:寻址困难,插入和删除容易。
       而哈希表可以综合二者的优点。事实上,哈希表有多种不同的实现方法,最经典的一种方法 —— 拉链法,我们可以将其理解为 链表的数组,如下图所示:
              

4.HashMap 的快速存取
     在HashMap中,我们最常用的两个操作就是:put(Key,Value) 和 get(Key)。
当我们调用put方法存值时,HashMap首先会调用Key的hashCode方法,然后基于此获取Key哈希码,通过哈希码快速找到某个桶,这个位置可以被称之为 bucketIndex。理论上,hashCode 可能存在碰撞的情况,当碰撞发生时,这时会取出bucketIndex桶内已存储的元素,并通过hashCode() 和 equals() 来逐个比较以判断Key是否已存在。如果已存在,则使用新Value值替换旧Value值,并返回旧Value值;如果不存在,则存放新的键值对<Key, Value>到桶中。因此,在 HashMap中,equals() 方法只有在哈希码碰撞时才会被用到。  

5.put方法分析
public V put(K key, V value) {

        //当key为null时,调用putForNullKey方法,并将该键值对保存到table的第一个位置
        if (key == null)
            return putForNullKey(value);

        //根据key的hashCode计算hash值
        int hash = hash(key);             //  ------- (1)

        //计算该键值对在数组中的存储位置(哪个桶)
        int i = indexFor(hash, table.length);              // ------- (2)

        //在table的第i个桶上进行迭代,寻找 key 保存的位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {      // ------- (3)
            Object k;
            //判断该条链上是否存在hash值相同且key值相等的映射,若存在,则直接覆盖 value,并返回旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;    // 返回旧值
            }
        }

        modCount++; //修改次数增加1,快速失败机制

        //原HashMap中无该映射,将该添加至该链的链头
        addEntry(hash, key, value, i);            
        return null;
    }


6.如何尽量保证元素均匀分布到table的每个桶中以便充分利用空间?    
(1)使用hash()方法对一个对象的hashCode进行重新计算是为了防止质量低下的hashCode()函数实现。
由于hashMap的支撑数组长度总是 2 的幂次,通过右移可以使低位的数据尽量的不同,从而使hash值的分布尽量均匀。
(2)static int indexFor(int hash, int length) {
        return h & (length-1);  
    }
    当length为2的n次方时,hash&(length - 1)就相当于对length取模(hash%(length)),而且速度比直接取模要快得多,
    这是HashMap在速度上的一个优化。
    
7.HashMap的扩容
resize()
  随着HashMap中元素的数量越来越多,发生碰撞的概率将越来越大,所产生的子链长度就会越来越长,
这样势必会影响HashMap的存取速度。
为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理,该临界点就是HashMap中元素的数量在数值上等于threshold(table数组长度*加载因子)。但是,不得不说,
扩容是一个非常耗时的过程,因为它需要重新计算这些元素在新table数组中的位置并进行复制处理(这个过程叫做重哈希)
所以,如果我们能够提前预知HashMap 中元素的个数,那么在构造HashMap时预设元素的个数能够有效的提高HashMap的性能。

8.get方法的分析    
HashMap只需通过key的hash值定位到table数组的某个特定的桶,然后查找并返回该key对应的value即可
public V get(Object key) {
        // 若为null,调用getForNullKey方法返回相对应的value
        if (key == null)
            // 从table的第一个桶中寻找 key 为 null 的映射;若不存在,直接返回null
            return getForNullKey();  

        // 根据该 key 的 hashCode 值计算它的 hash 码
        int hash = hash(key.hashCode());
        // 找出 table 数组中对应的桶
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //若搜索的key与查找的key相同,则返回相对应的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
9.HashMap与Hashtable的区别?   
        Hashtable的命运和Vector雷同!他们都是线程安全的,jdk1.0的产物
        Hashtable不能存储null键null值;线程安全
        HashMap可以存储null键null值;线程不安全

        HashMap已经全面的取代了Hashtable;   



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值