HashMap的源码解析

本文详细分析了HashMap在Java 1.7和1.8版本的实现,包括数据结构、PUT过程、数组初始化、哈希计算、扩容机制以及为何数组长度为2的幂次方。1.8版本引入了红黑树以优化长链表的性能,文章还探讨了HashMap的get方法和线程安全问题。

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

看了不少的hashmap解析源码的文章,然后自己也想总结记录一下,很久就想抽点时间写一篇关于hashmap的文章了,但现实就是不允许,原因有很多,主要还是太菜了,哈哈,现在终于有时间写了。

这篇文章分为hashmap1.7和1.8的解析,自己也不知道怎么开头,也是想到什么写什么,文章顺序可能会有点乱,望各位谅解。现在是北京时间:2019年4月2日 01:30:19。

HashMap1.7解析

首先看看一张图片来了解下HashMap的结构把

在这里插入图片描述
简单说下hashmap的数据结构把。
数组+链表
数组在知道下标之后查询速度尤其快,O(1)的时间复杂度。
链表在增删的时候速度非常快,找到位置后(前提),处理只需要O(1)的时间复杂度,因为不需要移动数据的位置,只需要更改指向的地址即可。但是链表在遍历对比的时候非常慢,时间复杂度为O(n),当然,我觉得这里的链表主要还是用来做哈希冲突时的解决方法,也称为拉链法。

//默认初始化化容量,即16  
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//最大容量,即2的30次方  
static final int MAXIMUM_CAPACITY = 1 << 30;  

//默认装载因子  
static final float DEFAULT_LOAD_FACTOR = 0.75f;  

//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态  
static final Entry<?,?>[] EMPTY_TABLE = {};  

//空的存储实体  
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  

//实际存储的key-value键值对的个数
transient int size;

//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold
int threshold;

//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;

//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;

//默认的threshold值  
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

PUT的过程分析

public V put(K key, V value) {
    // 当插入第一个元素的时候,需要先初始化数组大小
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 为 null,最终会将这个 entry 放到 table[0] 中
    if (key == null)
        return putForNullKey(value);
    // 1. 计算key 的 hash 值
    int hash = hash(key);
    // 2. 找到对应的数组下标
    int i = indexFor(hash, table.length);
    // 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,如果有,直接覆盖,put 方法返回旧值就结束了。
    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++;
    // 4. 不存在重复的 key,将此 entry 添加到链表中。
    addEntry(hash, key, value, i);
    return null;
}

数组初始化

private void inflateTable(int toSize) {
    // 比如这样初始化:new HashMap(20),那么处理成初始数组大小是 32,保证数组的长度是2的n次方
    int capacity = roundUpToPowerOf2(toSize);
    // 计算扩容阈值:capacity * loadFactor
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 初始化数组
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity); 
}
这里有个小问题,无论是1.7还是1.8的hashmap都是要将数组的大小初始化为2的n次方,该问题留在后面讲。。

计算该key在数组的位置

static int indexFor(int hash, int length) {
	//hash是key的hash值,length为数组长度。
    return hash & (length-1);
}

其实这里有个小问题,有些面试官也会问到,hash % length和 hash & (hash - 1)为什么不直接模 ,而要减一取余?是因为那样计算相对于模的话效率更高一点。各位可以自行测试,我在这里就不举例了。

添加节点到链表中

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值