HashMap源码分析

HashMap

**核心属性分析:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;    
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8; 
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table; 
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;   
    // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;
    // 填充因子,大概意思就是留出0.25给新数据
    final float loadFactor;  //默认值为0.75
}

1.构造

1.1HashMap()
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted    //填充因子默认值为0.75
} //默认初始容量是16,但要到插入时才会创建数组
1.2HashMap(int)

直接得到初始容量,还是会给默认的填充因子

1.3HashMap(int,float)

校验两个数的合理性

0<initialCapacity<=2^30

loadFactor>0

最需要注意的还是下面转化为2的次方数

this.threshold = tableSizeFor(initialCapacity);
        //转换得到2的次方数(返回一个>=当前initialCapacity的一个数)
1.4HashMap(Map<? extends K, ? extends V>)

2.增

public V put(K key, V value) { 
        return this.putVal(hash(key), key, value, false, true);   //调用putVal
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
     //tab:引用当前hashMap的散列表
     //p:表示当前散列表的元素
     //n:表示散列表数组的长度
     //i:表示路由寻址结果

    //延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中最耗费内存的散列表
    java.util.HashMap.Node<K,V>[] tab; java.util.HashMap.Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //寻址找到的桶位,刚好是null,这个时候,直接将当前k、v对应的node放进去
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

    else {
        //e:不为null的话,找到了一个与当前要插入的key-value一致的key的元素
        //k:临时的一个key
        java.util.HashMap.Node<K,V> e; K k;
        //表示桶位中的该元素,与你当前插入的元素的key完全一致,后续需要进行替换操作
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

        else if (p instanceof java.util.HashMap.TreeNode)
            e = ((java.util.HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        else {
            //链表的情况,而且链表的头元素与我们要插入的key不一致
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);  //插到链表末尾(9)

                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  树化
                        treeifyBin(tab, hash);
                    break;
                }
                //找到相同key的node元素,需要进行替换操作
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //e不等于null,说明找到了一个与你插入元素key完全一致的数据,需要进行替换
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold) //size自增,大于扩容临界值时,触发扩容
        resize();  //扩容
    afterNodeInsertion(evict);
    return null;
}

辅助分析图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0lL1zQ9-1617369220159)(C:\Users\大懒\Desktop\Java学习内容·\集合分析\putVal.PNG)]

3.删

final HashMap.Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    HashMap.Node[] tab;    //引用当前hashMap中的散列表
    HashMap.Node p;        //当前的node元素
    int n;                 //表示散列表数组长度
    int index;             //表示寻址结果
    if ((tab = this.table) != null && (n = tab.length) > 0 && (p = tab[index = n - 1 & hash]) != null) {
        //说明桶位有数据,需要进行查找操作并且删除
        HashMap.Node<K, V> node = null;   //查找到的结果
        Object k;
        //第一种情况,当前桶位中的元素就是要删除的元素
        if (p.hash == hash && ((k = p.key) == key || key != null && key.equals(k))) {
            node = p;
        }

        else {
            HashMap.Node e;
            if ((e = p.next) != null) {//说明当前桶位要么是链表要么是红黑树

                if (p instanceof HashMap.TreeNode) {    //判断当前桶位是否升级位红黑树

                    node = ((HashMap.TreeNode)p).getTreeNode(hash, key);  //第二种情况:进行红黑树查找操作
                }
                else {     //第三种情况,链表
                    label88: {
                        while(e.hash != hash || (k = e.key) != key && (key == null || !key.equals(k))) {
                            p = e;                          //循环查找链表上的键值
                            if ((e = e.next) == null) {
                                break label88;
                            }
                        }

                        node = e;
                    }
                }
            }
        }

        //下面为删除逻辑
        Object v;
        //判断node不为空的话,说明按照key查找到需要删除的数据
        if (node != null && (!matchValue || (v = ((HashMap.Node)node).value) == value || value != null && value.equals(v))) {

            //第一种情况:node是树节点,说明需要进行树节点移除操作
            if (node instanceof HashMap.TreeNode) {
                ((HashMap.TreeNode)node).removeTreeNode(this, tab, movable);
            }
            //第二种情况:桶位元素即为查找结果,则将元素下一个元素放入桶中
            else if (node == p) {
                tab[index] = ((HashMap.Node)node).next;
            }

            else {  //第三种情况:将当前元素p的下一个元素设置成 要删除元素的 下一个元素
                p.next = ((HashMap.Node)node).next;
            }

            ++this.modCount;
            --this.size;
            this.afterNodeRemoval((HashMap.Node)node);
            return (HashMap.Node)node;
        }
    }

    return null;
}

4.改

5.查

1.先判断桶中有没有数据,没有就直接退出

​ 2.有数据:第一种情况:正好定位的桶位元素和想查找的数据一致

​ 第二种情况:桶位不止一个元素,可能是链表也可能是红黑树

​ 3.桶位升级为了红黑树,红黑树查找

​ 桶位形成链表,链表查询得到相同的键位、hash值;返回元素,否则就循环查询直至结束

6.扩容

HashMap的容量变化通常存在以下几种情况:

  1. 空参数的构造函数:实例化的HashMap默认内部数组是null,即没有实例化。第一次调用put方法时,则会开始第一次初始化扩容,长度为16。

    newCap = 16;
    newThr = 12;   //初始化扩容
    
  2. 有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的2的幂数,将这个数设置赋值给阈值(threshold)。第一次调用put方法时,会将阈值赋值给容量 。

    newCap = oldThr;
    
  3. 如果不是第一次扩容,则容量和阈值都变为原来的2倍时,负载因子还是不变

    //扩容之前的table数组大小已经达到最大阈值后,不进行扩容,将扩容阈值设置为int最大值
    if (oldCap >= 1073741824) {
        this.threshold = 2147483647;
        return oldTab;
    }
    //oldCap左移一位实现数值翻倍,并且赋值给newCap,newCap小于数组最大值限制,并且扩容之前的阈值大于等于16
    //此时进行下一次扩容的阈值等于当前阈值翻倍
    if ((newCap = oldCap << 1) < 1073741824 && oldCap >= 16) {
        newThr = oldThr << 1;
    }
    

注意!!!

  • 首次put时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;
  • 不是首次put,则不再初始化,直接存入数据,然后判断是否需要扩容;

采用什么数据结构?

数组+链表+红黑树实现

如何实现的?为什么这么实现?

线程是否安全?

说一下工作原理?

说一下每个函数的执行过程?

与其他数据结构的区别?

这几天写的集合源码总结,希望对有需要的人有点用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值