HashMap的来龙去脉

本文深入解析HashMap的实现原理,包括数据结构的选择、构造函数、put操作的流程,特别是为何数组长度选择2的次幂以及如何处理哈希碰撞。讨论了扩容策略以及Java 1.8后采用尾插法的原因,同时提到了HashMap在实际使用中的注意事项,并引入了Android中的SparsArray和ArrayMap作为对比。
摘要由CSDN通过智能技术生成

我们学习数据结构的时候,最开始入门的也是最基础的两种数据结构是线性表和链表结构。

线性表以数组为代表,数组我们平时很常用,我们都知道数组有个优点就是查询和修改很快这个具体的原理是因为数组在申请内存时,申请的是一块连续的相等大小的内存,那么我们在查找某个下标的元素的时候,只要知道了首地址、下标、每个元素所占的内存,那么我们很快就可以计算出目标地址,一下子就找到,其时间复杂度为O(1),但是数组的增删效率比较低,那是因为我们在增加或者删除一个元素的时候,该元素往后的其余元素都要随之逐个移动,这个移动的时间复杂度是O(n);

那还有另一种数据结构是链表,链表由一个一个的节点连接而成,链表在内存中存储并不是连续的,所以每个节点除了保存数据外,还需要额外一个指针保存下一个节点的地址(我们这以单向链表为例吧),链表有个优点,就是增删效率很高,比如我们要删除某节点X,只要让X的前一个节点指针指向X的后一个节点,然后释放掉X的内存即可,时间复杂度为O(1),但是链表的查找和修改效率就比较低了,如果我们要找个某一个元素,则需要从第一个节点开始逐个遍历才行,所以其时间复杂度为O(n)。

这两种数据结构各有各的优点,在实际开发中,如果查询多就用数组,增删多就用链表。

那么有没有一种数据结构可以吧这两种数据结构的优点都结合起来呢~没错,写这篇文章就是为了介绍HashMap!

HashMap的原理细讲的话内容太长,我们抓几个关键的点来入手吧。

我们点进源码从它的几个常量开始看:

    // 默认初始化长度
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 最大的容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认加载因子,当map中已使用的数组长度占到总长度的75%时进行扩容,扩容比为两倍
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 转换成树结构的阈值,至于为什么是8,要根据概率论中的泊松分布原理,超过8后的概率已经足够小了
    static final int TREEIFY_THRESHOLD = 8;

    // 取消树结构的阈值
    static final int UNTREEIFY_THRESHOLD = 6;

    // 转换成红黑树的最小数组长度
    static final int MIN_TREEIFY_CAPACITY = 64;

从常量上看,很容易误以为HashMap的默认长度是16,然而并不是,那我们就看看构造函数吧:

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }


    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

当我们不传初始长度的时候就会使用默认的长度(但是其实这时候还没有进行HashMap结构的初始化),如果传了长度,就会走两个参数的构造方法了。在方法中,首先对传入的参数进行判断是否合法,我们看到里面用到一个方法Fload.isNaN(),这是因为在Float类中定义了一个值 public static final float NaN = 0.0f / 0.0f;这个是一个未知的值,所以不允许使用,校验完参数之后,构造函数里面有一个操作:

this.threshold = tableSizeFor(initialCapacity);

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值