HashMap底层原理:JDK1.6 VS 1.8

本文深入探讨了JDK1.6和1.8中HashMap的底层原理,包括数据结构(数组+链表 vs 数组+链表+红黑树)、哈希算法的优化、put()和get()方法的源码解析,以及扩容策略的差异。在1.8中,HashMap引入了红黑树,优化了哈希算法,并改进了扩容算法,提高了性能。

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

在JDK1.6和1.8下, HashMap底层发生了较大的改变,总结如下。

Ⅰ.JDK1.6下的HashMap底层原理

一、HashMap数据结构

HashMap的底层数据结构是数组+链表,利用数组来实现快速定位,链表来解决哈希冲突。

二、哈希算法

好的哈希算法应尽可能保证计算简单和散列地址均匀,这样可以减少哈希冲突。在HashMap中,哈希算法包括两部分:扰动计算和数组索引计算。
扰动计算 在HashMap中,首先采用扰动函数对key的hashCode进行扰动计算(代码如下):通过若干次的移位、异或操作,把高位的特征和低位的特征组合起来,减少高位不同,低位相同带来的哈希冲突。

static int hash(int h) {
   
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

数组索引计算 哈希值对数组长度取余是一个常见的哈希函数,当底层数组的长度为2的n次方时,h % length相当于h & (length-1)。与操作比求余操作效率要高,这也是为什么HashMap的底层数组长度总是2的n次方

static int indexFor(int h, int length) {
   
         return h & (length-1);
     }

三.重要属性

    static final int DEFAULT_INITIAL_CAPACITY = 16;//默认数组初始容量
   
    static final int MAXIMUM_CAPACITY = 1 << 30;//默认数组最大容量
    
    static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认装载因子
    
    transient Entry[] table;//存储数据的Entry类型数组,长度是2的幂
    
    transient int size;//保存的键值对数量
    
    int threshold;//需要调整数组大小时的阈值(容量*装载因子)
    
    final float loadFactor;//装载因子
    
    transient volatile int modCount;//map结构被改变的次数

四.put()方法源码

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

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

在检查节点时,光检查hash值是不够的,因为key相同,hash值一定相同,但是hash值相同,key不一定相同,所以在检查节点时,既检查了hash值也检查了key:e.hash == hash && ((k = e.key) == key || key.equals(k))。有一道经典的面试题也和它类似:“hashcode相等,equals是否相等”,hashcode相等,equals不一定相等,equals相等,hashcode一定相等。

代码详解1.putForNullKey

存入的键key为null时,HashMap进行一个特殊处理,将它存放在数组索引为0处。

    /**
     * Offloaded version of put for null keys
     */
    private V putForNullKey(V value) {
   
        
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
      
            if (e.key == null) {
      
                V oldValue = e.value;
                e.value = value;//已存在null键时,对value进行覆盖
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;        
        addEntry(0, null, value, 0);       
        return null;
    }

代码详解2.addEntry

当插入的键key不为null时,通过哈希算法计算对应的数组索引i,遍历相对应的链表,当链表中已经存在键为key的索引时,对value进行覆盖,否则,在链表头部插入新的Entry节点。之后判断键值对数量(size)是否到达了需要扩充table数组容量的界限(threshold)并让size自增1,如果达到了则调用resize(int capacity)方法拓展数组容量。

1 void addEntry(int hash, K key, V value, int bucketIndex) {
   
2     Entry<K,V> e = table[bucketIndex];
3         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
4         if (size++ >= threshold)
5             resize(2 * table.length);
6     }

扩容算法详解:resize

当数组容量到达上限时,更新需要调整数组大小时的阈值(threshold)为int的最大值,不进行扩容。每一次扩容时,将数组容量扩展为原来的两倍(满足2的n次方),并将旧数组中的内容迁移至新数组:对链表中的每一个节点,重新计算它在新数组中的索引,并插入到相应的链表头部。最后,设置threshold为新数组容量*装载因子。因此,在JDK1.6中,扩容操作的代价是很大的

void resize(int newCapacity) {
   
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
   
            threshold = Integer.MAX_VALUE;
            return;
        }
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);//将旧数组中的内容迁移至新数组
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }
void transfer(Entry[] newTable) {
   
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
   //遍历数组的每一个索引
            Entry<K,V> e = src[j];
            if (e != null) {
   
                src[j] = null;
                do {
   //遍历链表
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];// 将节点e插入到newTable[i]指向的链表的头部
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

五、get()方法源码

public V get(Object key) {
   
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        //计算键值key对应的索引,遍历对应的链表;当节点的哈希值、键值与目标哈希值,键值相等,则该节点为目标节点,返回目标节点的value.
        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;
    }

代码详解1.getForNullKey

get操作的源码简单,这里就不详细说了。同样的,当查询的键key为null时,仅需在数组索引为0处的链表进行遍历查找。

private V getForNullKey() {
   
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
   
            if (e.key == null)
                return e.value;
        }
        return null;
    }

Ⅱ.JDK1.8下的HashMap优化

JDK1.8从三方面对HashMap进行了优化,
①.底层数据结构由数组+链表变为数组+链表+红黑树:而当链表长度太长(TREEIFY_THRESHOLD默认超过8)时,链表就转换为红黑树。当长度小于(UNTREEIFY_THRESHOLD默认为6),就会退化成链表。
②.优化了哈希算法:在进行扰动计算时,JDK1.6中进行了4次的右位移异或混合操作,而JDK1.8中简化为一次。

static final int hash(Object key) {
   //JDK1.8中的扰动计算
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

③.优化了扩容算法:JDK1.8充分利用了一个特性:当数组的容量变为原来的两倍,那么元素的位置要么是原位置,要么是原位置+旧数组容量。

JDK 1.8中,ConcurrentHashMap的实现进行了完全重构。相对于JDK 1.7的版本,JDK 1.8的ConcurrentHashMap的数据结构更接近HashMap。它使用了synchronizedCAS(Compare and Swap)来进行同步操作,而不再使用分段锁的概念。锁的粒度也降低了,不再是基于Segment的锁,而是对每个桶(数组中的头节点)加锁。这样的改变使得操作更加清晰流畅。此外,JDK 1.8还使用红黑树来优化链表,当链表长度超过一定阈值时,会将链表转换为红黑树,提高遍历效率。总的来说,JDK 1.8的ConcurrentHashMap的实现更简单,操作更清晰流畅,同时也提高了性能。\[1\]\[2\] 另外,关于synchronized的变化,JDK 1.8之前的版本中,synchronized是通过底层的操作系统Mutex Lock来实现的,这种锁被称为"重量级锁"。由于锁的获取释放需要从用户态转换到核心态,导致性能较低。但是在JDK 1.6之后,为了提高性能,引入了"轻量级锁""偏向锁"的概念,使得synchronized的效率与ReentrantLock相差无几,甚至在某些场景下更优于ReentrantLock。因此,在JDK 1.8中,synchronized的性能得到了提升。\[3\] #### 引用[.reference_title] - *1* [JDK1.8的ConcurrentHashMap底层实现解析](https://blog.csdn.net/qq_51901495/article/details/126320698)[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] - *2* [Java集合源码剖析——基于JDK1.8中ConcurrentHashMap的实现原理](https://blog.csdn.net/weixin_43823808/article/details/126732474)[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* [Synchronized原理jdk1.8后的优化](https://blog.csdn.net/qq_35152037/article/details/105463688)[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、付费专栏及课程。

余额充值