资讯专栏INFORMATION COLUMN

浅析HashMap源码(1)

wwolf / 3180人阅读

摘要:前言本文的目的是阅读理解的源码,作为集合中重要的一个角色,平时用到十分多的一个类,深入理解它,知其所以然很重要。

前言

本文的目的是阅读理解HashMap的源码,作为集合中重要的一个角色,平时用到十分多的一个类,深入理解它,知其所以然很重要。本文基于Jdk1.7,因为Jdk1.8改变了HashMap的数据结构,进行了优化,我们先从基础阅读,之后再阅读理解Jdk1.8的内容

HashMap的特性

1.通过key-value的形式快速的存取元素
2.允许键为Null,但只允许有一个键的值为Null
3.线程不安全
4.底层结构是Hash表,元素是无序的
5.再不考虑Hash冲突的时候,插入和查询的复杂度是可以达到O(1)的

HashMap的数据结构

底层数据结构是一个Hash表,基于数组和链表,数组里面保存着一个单向链表的头节点,单项链表保存着具有相同Hash值的不同元素,再不发生Hash冲突的情况下,链表应该只有一个元素,这是最理想的状态。


链表的数据结构代码
`

static class Entry implements Map.Entry {
final K key;
V value;
Entry next;    // 下一个Entry对象的引用
int hash;    // 其实就是key的hash值  
}         
HashMap的常量结构
// 默认初始化容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
 
// HashMap允许的最大容量 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
 
// 默认的负载率 75%
static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
// 空的哈希表
static final Entry[] EMPTY_TABLE = {};
 
// 实际使用的哈希表
transient Entry[] table = (Entry[]) EMPTY_TABLE;
 
// HashMap的大小,即存储的key-value的数量
transient int size;
 
// 扩容的阀值,当HashMap的size达到阀值时,就开始扩容 threshold=length*threshold 
int threshold;
 
// 负载率
final float loadFactor;
 
// 修改次数, 用于fail-fast机制
transient int modCount;
 
// 替代哈希使用的默认扩容阀值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
 
// 随机的哈希种子, 有助于减少发生哈希碰撞的几率

transient int hashSeed = 0;
HashMap的初始化

HashMap的初始化涉及到上面的多个常量,在了解完常量的作用之后,我们就可以理解HashMap的初始化思想,首先,HashMap并不是通过构造函数来初始化的,构造函数只是初始化HashMap的初始化参数,包括DEFAULT_INITIAL_CAPACITY ,loadFactor等,再初始化参数之后,真正的调用Put方法时,会判断table 是否已经初始化,没有的话再根据参数进行初始化。

put方法的流程我们这边也要先理解:
(1)检查哈希表是否是个空表,如果是空表就调用inflateTable方法进行初始化
(2)判断key是否为null,如果为null,就调用putForNullKey方法, 将key为null的key-value存储在哈希表的第一个位置中
如果key不为null,则调用hash方法计算key的hash值
(3)根据hash值和Entry数组的长度定位到Entry数组的指定槽位
(4)判断Entry数组指定槽位的值e是否为null, 如果e不为null, 则遍历e指向的单链表, 如果传入的key在单链表中已经存在了, 就进行替换操作, 否则就新建一个Entry并添加到单链表的表头位置
(5)如果e为null, 就新建一个Entry并添加到指定槽位

下面是代码:
构造方法

public HashMap(int initialCapacity, float loadFactor) {
    // 如果初始容量小于0,则抛出异常
    if (initialCapacity < 0) {
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    }
 
    // 如果初始容量大于容量最大值,则使用最大值作为初始容量
    if (initialCapacity > MAXIMUM_CAPACITY) { initialCapacity = MAXIMUM_CAPACITY; }
 
    // 如果负载率小于等于0或负载率不是浮点数,则抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    }
 
    // 设置负载率
    this.loadFactor = loadFactor;
 
    // 设置阀值为初始容量
    threshold = initialCapacity;
 
    // 空实现, 交由子类实现
    init();
}
//

初始化数组方法

private void inflateTable(int toSize) {
    // 寻找大于toSize的,最小的,2的n次方作为新的容量
    int capacity = roundUpToPowerOf2(toSize);
 
    // 阀值=容量*负载因子, 如果容量*负载因子>最大容量时, 阀值=最大容量
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
 
    // 按新的容量创建一个新的数组
    table = new Entry[capacity];
 
    // 重新初始化hashSeed
    initHashSeedAsNeeded(capacity);
}

put方法

public V put(K key, V value) {
    // 如果哈希表没有初始化就进行初始化
    if (table == EMPTY_TABLE) {
        // 初始化哈希表
        inflateTable(threshold);
    }
 
    // 当key为null时,调用putForNullKey方法,保存null于table的第一个位置中,这是HashMap允许为null的原因
    if (key == null) {
        return putForNullKey(value);
    }
 
    // 计算key的hash值
    int hash = hash(key);
    // 根据key的hash值和数组的长度定位到entry数组的指定槽位
    int i = indexFor(hash, table.length);
    // 获取存放位置上的entry,如果该entry不为空,则遍历该entry所在的链表
    for (Entry e = table[i]; e != null; e = e.next) {
        Object k;
        // 通过key的hashCode和equals方法判断,key是否存在, 如果存在则用新的value取代旧的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;
        }
    }
 
    // 修改次数增加1
    modCount++;
    // 如果找不到链表 或者 遍历完链表后,发现key不存在,则创建一个新的Entry,并添加到HashMap中
    addEntry(hash, key, value, i);
    return null;
}

 void addEntry(int hash, K key, V value, int bucketIndex) {
    //添加key到table[bucketIndex]位置,新的元素总是在table[bucketIndex]的第一个元素,原来的元素后移
    Entry e = table[bucketIndex];
        table[bucketIndex] = new Entry(hash, key, value, e);
    //判断元素个数是否达到了临界值,若已达到临界值则扩容,table长度翻倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }
HashMap的查

当key值为Null的时候会进行特殊处理,在table[0]的链表上查找Key为null的元素,get的过程是:
(1)计算hash与table.length取模计算index值
(2)遍历table[index]上的链表,直到找到key

 void addEntry(int hash, K key, V value, int bucketIndex) {
//添加key到table[bucketIndex]位置,新的元素总是在table[bucketIndex]的第一个元素,原来的元素后移
Entry e = table[bucketIndex];
    table[bucketIndex] = new Entry(hash, key, value, e);
//判断元素个数是否达到了临界值,若已达到临界值则扩容,table长度翻倍
    if (size++ >= threshold)
        resize(2 * table.length);
}

#HashMap的删
remove方法同样也是,先计算hash,在计算index,遍历查找,找到之后删除节点

/**
     * 根据key删除元素
     */
    public V remove(Object key) {
        Entry e = removeEntryForKey(key);
        return (e == null ? null : e. value);
    }

    /**
     * 根据key删除链表节点
     */
    final Entry removeEntryForKey(Object key) {
        // 计算key的hash值
        int hash = (key == null) ? 0 : hash(key.hashCode());
        // 根据hash值计算key在数组的索引位置
        int i = indexFor(hash, table.length );
        // 找到该索引出的第一个节点
        Entry prev = table[i];
        Entry e = prev;

        // 遍历链表(从链表第一个节点开始next),找出相同的key,
        while (e != null) {
            Entry next = e. next;
            Object k;
            // 如果hash值和key都相等,则认为相等
            if (e.hash == hash &&
                ((k = e. key) == key || (key != null && key.equals(k)))) {
                // 修改版本+1
                modCount++;
                // 计数器减1
                size--;
                // 如果第一个就是要删除的节点(第一个节点没有上一个节点,所以要分开判断)
                if (prev == e)
                    // 则将下一个节点放到table[i]位置(要删除的节点被覆盖)
                    table[i] = next;
                else
                 // 否则将上一个节点的next指向当要删除节点下一个(要删除节点被忽略,没有指向了)
                    prev. next = next;
                e.recordRemoval( this);
                // 返回删除的节点内容
                return e;
            }
            // 保存当前节点为下次循环的上一个节点
            prev = e;
            // 下次循环
            e = next;
        }

        return e;
}
HashMap的扩容

resize扩容是HashMap中非常重要的一个操作,在容器里的元素达到一个临界值时,HashMap会自动进行扩容,扩容的具体流程是;
1.在put的时候检查是否需要扩容,根据两个参数:初始容量和装载因子
2.创建一个容量为table.length*2的table,修改临界值
3.重新计算所有元素的hash值,并放入新的table,使用的是头插法
4.用新的table替换旧的table

 void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//新建一个新表
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
        transfer(newTable, rehash);//完成旧表到新表的转移
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
--------------------- 
 void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry e : table) {//遍历同桶数组中的每一个桶
            while(null != e) {//顺序遍历某个桶的外挂链表
                Entry next = e.next;//引用next
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
                e.next = newTable[i];//头插法插入新表中
                newTable[i] = e;
                e = next;
            }
        }
    }

扩容的整体操作如上,但是有一些十分精妙的细节十分厉害

为什么扩容的容量一定是2的幂?

这么设计当然是为了性能,而且是十分显著的性能提升,涉及到了位操作,我觉得非常有意思,会在下一篇专门讲这样计算进行提升性能的例子。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77765.html

相关文章

  • JAVA HashMap源码浅析

    摘要:类的属性和构造函数二的初始化构造方法这是的构造函数之一,其他构造函数都引用这个构造函数进行初始化。在构造函数中不会对数组进行初始化,只有在等操作方法内会进行判断是否要初始化或扩容。其作用是保证的效率。 引言 HashMap在键值对存储中被经常使用,那么它到底是如何实现键值存储的呢? 一 Entry Entry是Map接口中的一个内部接口,它是实现键值对存储关键。在HashMap中,有E...

    xuxueli 评论0 收藏0
  • 浅析微信支付:查询订单和关闭订单

    摘要:本文是浅析微信支付系列文章的第七篇,主要讲解微信商户平台的订单查询和关闭接口的使用。查询订单以下为微信官方的查询订单文档应用场景该接口提供所有微信支付订单的查询,商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。 本文是【浅析微信支付】系列文章的第七篇,主要讲解微信商户平台的订单查询和关闭接口的使用。 浅析微信支付系列已经更新六篇了哟~,没有看过的朋友们可以看一下哦。 ...

    Dean 评论0 收藏0
  • HashMap 浅析 —— LeetCode Two Sum 刷题总结

    摘要:注意这里我说的是一般情况下,因为哈希算法需要兼顾性能与准确性,是有一定概率出现重复的情况的。哈希算法实际上是数学家和计算机基础科学家研究的领域。 背景 做了几年 CRUD 工程师,深感自己的计算机基础薄弱,在看了几篇大牛的分享文章之后,发现很多人都是通过刷 LeetCode 来提高自己的算法水平。的确,通过分析解决实际的问题,比自己潜心研究书本效率还是要高一些。 一直以来遇到底层自己无...

    zoomdong 评论0 收藏0
  • 浅析微信支付:如何使用沙箱环境测试

    摘要:本文是浅析微信支付系列文章的第十篇,主要讲解如何使用沙箱环境来测试微信支付。图为微信支付仿真测试系统后简称仿真系统的简化原理图。沙箱说明微信支付沙箱环境,是提供给微信支付商户的开发者,用于模拟支付及回调通知。 本文是【浅析微信支付】系列文章的第十篇,主要讲解如何使用沙箱环境来测试微信支付。 浅析微信支付系列已经更新十篇了哟~,没有看过的朋友们可以看一下。 浅析微信支付:下载对账单和资...

    骞讳护 评论0 收藏0
  • 浅析微信支付:下载对账单和资金账单

    摘要:本文是浅析微信支付系列文章的第九篇,主要讲解商户下载对账单接口和资金账单接口的实现和一些注意事项。注意微信侧未成功下单的交易不会出现在对账单中。 本文是【浅析微信支付】系列文章的第九篇,主要讲解商户下载对账单接口和资金账单接口的实现和一些注意事项。 浅析微信支付系列已经更新九篇了哟~,没有看过的朋友们可以看一下哦。 浅析微信支付:申请退款、退款回调接口、查询退款 浅析微信支付:查询订...

    Ethan815 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<