HashMap底层源码逻辑解析(轻松掌握)

HashMap基于jdk1.8的底层数据结构包括数组、链表和红黑树。默认加载因子为0.75,以平衡冲突和空间利用率。构造方法允许指定容量和加载因子,容量必须为2的幂。put方法涉及哈希计算和处理冲突,当链表超过一定长度时会转为红黑树,提高查询效率。
摘要由CSDN通过智能技术生成

一,引言:HashMao基于jdk1.8的底层数据结构:

数组的查询效率为O(1),链表的查询效率是O(k),红黑树的查询效率是O(log n),所以当元素量很大时,红黑树的效率就体现出来了。

二,HashMap的构造方法

1,无参构造:

HashMap<String, String> map = new HashMap<String, String>();
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
     //加载因子--0.75
     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
无参构造时,HashMap自动构造了一个默认的加载因子0.75;(这个加载因子的最大容量为16)

因子的大小表示每一个Hash桶中所能填满元素多少,因子越大,所能填满的元素越多;好处是,空间的利用率增加了;坏处是,冲突的机会加大了。
由于HashMap遇到冲突便会转换为链表储存或是红黑树储存,当需要查询一个数据时,就会增加查询次数,增加查询的成本。

为什么默认的加载因子为0.75?原因是因为因子大了,冲突的可能就会增加,而因子小了,就会使空间浪费更多,并且会增加扩容的操作次数;所
以在“冲突增加”与“空间利用”之间,寻找一种平衡与折中,这就是为什么因子为0.75。

HashMap的默认容量是:0.75*16=12;

2,带参构造:

//构造一个容量
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
//构造容量和加载因子
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
//进行判断,如果传入参数小于0,抛出异常。
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
//如果传入参数大于容量最大的参数,默认设置为最大容量。
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
//isNaN:is not a Number;
            //判断加载因子参数,isNaN(i):返回一个Boolean值,如果i为NaN值,那么isNaN返回true,反之返回一个false。
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
//当因子参数小于等于0或不为NaN值时,抛出异常。
        this.loadFactor = loadFactor;//加载因子赋值
        //阈值---控制扩容---起到限制容量的作用,扩容阈值threshold = loadFactor*capacoty
        this.threshold = tableSizeFor(initialCapacity);//
//tableSizeFor这个方法是找到一个离容量参数最近的2的n次方数例,比如cap = 14,return 16;
 }

3,tablesSizeFor方法:

static final int tableSizeFor(int cap) {
    //-1可以保证当传入的数刚好是2的次方时,可以正确的返回其本身,例:传入的是16,经过下面的计算后还是返回16
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

//这是HashMap默认的规则,其容量大小(桶的数量)必须为2的n次方数。原因是:
//HashMap为了存取的高效,就尽量把数据分配均匀,关键就是判断把当前数据存放到哪一个桶中,这个算法就是取模运算。
//当前key的Hashcode % HashMap的容量,但是由于取模运算的效率不如位运算(&),位运算就是根据二进制按位运算,1遇1
//得1,其他为0,比如1100 & 1010 = 1000;这样明显逻辑更为简单,更加快速。
//当容量为2的n次方时,hashcode&(HashMap的容量 - 1)==hashcode%HashMap的容量;于是位运算替换了取模运算,而前提就是容量为2的n次方。

三,HashMap的put方法

map.put(k,v);
↓
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
//put进一组kv时,当kv存进数组后,会返回一个null;如果过程为替换,则会把被替换的值返回。

//根据传过来的key-调用他hashCode方法,通过高16和低16位的异或运算增加随机性
//计算key的hashcode值进行比较或是选择放进哪一个hash桶中
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        //定义了几个局部临时变量
        Node<K,V>[] tab; //数组可以暂存所有hash桶
        Node<K,V> p;//暂存一个桶中的节点或链表
        int n, i;
//transient Node<K,V>[] table;--成员变量--这个就是hashMap的底层实现,相当于
//ArrayList里面的elementData。


//扩容
//↓
if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
//当put数组的长度达到了扩容的阈值threshold之后,调用resize()方法进行扩容。
//扩容逻辑:如果是带参构造:1,将用户传入的容量参数*用户传入的负载因子loadFactor的计算结
//赋值给扩容阈值threshold。2,将容量参数赋值给新的容器,根据新容器创建出一个新的node数组
//无参构造:1,将 默认负载因子0.75f * 默认的容量大小DEFAULT_INITIAL_CAPACITY,的计算结果,赋值给扩容阀值threshold。
//2,将默认容量大小DEFAULT_INITIAL_CAPACITY(16)赋给新的Node数组;根据新的Node数组创建出一个新的Node数组。
//n中存的是扩容后临时的tab的长度。

 if ((p = tab[i = (n - 1) & hash]) == null)

//如果这个桶里位空,直接放入桶中的第一个位置。
//tab[15&hash]位运算计算key放到哪个hash桶里。(接上面说的为什么使用位运算)

//如果这个桶不为空。
//↓
else {
//hash冲突了----一个节点,一个链表,红黑树
 Node<K,V> e; //暂存一个节点---重复的节点
K k;//键
// 如果桶中第一个元素的key与待插入元素的key相同,保存在e中e来记录,后续通过e判断是否直接return
if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
                //说明跟桶中的key值重复了。
          e = p;
// 如果这个p是树节点,则调用树节点的putTreeVal插入元素
    else if (p instanceof TreeNode)
          e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//说明是链表    
//遍历这个桶对应的链表,binCount用于存储链表中元素的个数    
        else {
            for (int binCount = 0; ; ++binCount) {
//链表遍历完后都没有找到相同的key的话,说明该key对应的元素不存在,则在链表末接上一个新的节点。
                if ((e = p.next) == null) {
          p.next = newNode(hash, key, value, null);
//这里判断插入新节点后,链表长度是否大于等于8,判断是否需要树化,因为第一个元素没有加到binCount中,所以这里-1
//如果数组的长度没有达到64,还是会继续扩容,不会树化。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
          treeifyBin(tab, hash);//树化
                        break;
}
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果不为空,说明这个节点的Key重复---就不会添加一个元素,而是替换
            if (e != null) { // existing mapping for key
    //记录下旧值
                V oldValue = e.value;
//判断是否替换旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//替换
                afterNodeAccess(e);//返回旧值
                return oldValue;
            }
        }
//modcount为修改次数,不论是增加或者删除,都会++。
        ++modCount;
        if (++size > threshold)
            resize();//元素数量++,判断是否达到扩容阈值,大于扩容阈值时,调用resize方法进行扩容。
        afterNodeInsertion(evict);
        return null;//没找到元素返回null。
}

四,HashMap put方法逻辑思维导图

五,树化的方法

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //链表要转红黑树得让hash桶的长度达到64
        //static final int MIN_TREEIFY_CAPACITY = 64;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {//树化
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

红黑树待更新。

  • 12
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
HashMap是Java中常用的一种数据结构,它底层采用的是哈希表的实现方式。下面是HashMap底层码分析: 1. HashMap的数据结构 HashMap的数据结构是一个数组,数组中的每个元素是一个链表,链表中存放了哈希值相同的键值对。当发生哈希冲突时,新的键值对将会添加到链表的末尾。在查找键值对时,首先根据键的哈希值在数组中定位到对应的链表,然后再在链表中查找键值对。这种实现方式的时间复杂度为O(1),但是在发生哈希冲突时,链表的长度会变长,查找效率也会降低。 2. HashMap的put方法 当向HashMap中添加键值对时,首先会计算键的哈希值,然后根据哈希值计算出在数组中的位置。如果该位置为空,则直接将键值对添加到该位置;否则,需要遍历链表,查找是否已经存在相同的键,如果存在,则将旧的值替换为新的值;如果不存在,则将新的键值对添加到链表的末尾。 3. HashMap的get方法 当从HashMap中获取键值对时,首先计算键的哈希值,然后根据哈希值在数组中定位到对应的链表。接着遍历链表,查找是否存在相同的键,如果存在,则返回对应的值;如果不存在,则返回null。 4. HashMap的扩容机制 当HashMap中的元素个数超过数组长度的75%时,会自动扩容。扩容时,会将数组长度扩大一倍,并将原来的键值对重新分配到新的数组中。重新分配时,需要重新计算键的哈希值和在新数组中的位置。这个过程比较耗时,但是可以避免链表过长导致的查找效率降低。 5. HashMap的线程安全性 HashMap是非线程安全的,因为在多线程环境下,可能会存在多个线程同时对同一个链表进行操作的情况,从而导致数据不一致。如果需要在多线程环境下使用HashMap,可以使用ConcurrentHashMap,它是线程安全的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一直在奋斗的鲨鱼辣椒!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值