JDK源码学习01-HashMap源码学习

HashMap源码学习

HashMap中直接注意的细节:

  1. 红黑树长度小于阈值(yu 4声) 6 转化成两边
  2. 链表长度大于阈值8 且table的长度大于等于64 才树化 仅满足链表长度大于阈值8只会调用resize扩容
为什么是6和8呢,而不设置成一样呢,因为为了防止在边界反复横跳,浪费性能
 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
  1. HashMap扩容时数据迁移
    a. 旧表槽为空 直接跳过
    b. 旧表槽中仅一个节点 求得新的索引再放入

    newTab[e.hash & (newCap - 1)] = e;
    c. 旧表中是红黑树
    红黑树中其实依旧维持了链表结构 故处理方法和链标一样
    d. 旧表中是链标
	首先  e.hash是不变的  这里设oldCap老表长度为16
	则老表的下标计算只是用了e.hash的低4位   已知e.hash&15=j
	由于表扩容了一倍 所以现在  e.hash的低5位都应该被使用
	而已经有了底4位使用的记录  如果e.hash的第五位是0  则数据所在位置和老表一样
	如果e.hash的第五位是1   则位置为j+oldCap即j+newTab.length/2
	因为如果e.hash的第五位是1  那么数据必然会存储在新表的后半段
	而低四位的值就相当于offset这里是j   就就是存储位置相对于newTab.length/2的偏差

在这里插入图片描述

静态成员

# 默认初始化容量,在第一次put时table才会初始化为该容量
# 如果new HashMap(n)  指定了长度  则直接table为该容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
# table最大容量2^30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
# 加载因子  当size>DEFAULT_LOAD_FACTOR*CAPACITY时会扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

# 树化阈值  链表长度大于阈值8 且table的长度大于64才树化  仅满足链表长度大于阈值8只会调用resize扩容
    static final int TREEIFY_THRESHOLD = 8;//大于8  切table长度大于64
# 最小树化容量   必须同时满足链表大于8   且长度大于64才能树化   只满足大于8就会扩容
    static final int MIN_TREEIFY_CAPACITY = 64;

# 取消树化阈值  当树的长度小于6的时候  会退化成链表  
	static final int UNTREEIFY_THRESHOLD = 6;//小于6

成员变量

# hashmap的哈希表
    transient Node<K,V>[] table;
    
# 保存缓存的entrySet()。请注意,抽象映射字段用于keySet()和values()。
    transient Set<Map.Entry<K,V>> entrySet;
    
# 当前大小
    transient int size;

# 表结构修改次数  此字段用于使HashMap集合视图上的迭代器快速失败
    transient int modCount;
    
# 下次需要扩容的大小 (capacity * load factor).  当size>threshold且table长度大于64才扩容
    int threshold;
    
# 实际的加载因子  可以在new HashMap(n,loadFactor)的时候指定
    final float loadFactor;
        public HashMap(int initialCapacity, float loadFactor)
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);

hash函数与tableSizeFor函数(静态工具函数)

hashcode生成的是32位的码,为了完整使用到32位,并更好的散列,就将高16位与低16位异或(如果相同则为0,如果不同则为1)
【原因:使用 | 会出现更多1 使用 & 会出现更多0 故都不合适】

    static final int hash(Object key) {
        int h;
        //  >>>表示右移后左边补0    所以就是32位h的高16位和低16位
        //  ^是异或  00 11为1    01 10为0
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这里我们使用一个demo来测试一下hash函数,我们可以发现,hash是在hashcode的基础上,把高16位保持不变,第十六位变成【高十六位和低16位异或】,保证了在不太大的容量的情况下更好的散列性

public static void main(String[] args) {
        Test2 test2 = new Test2();
        int hashCode = test2.hashCode();
        int hash = hash(test2);
        System.out.println(hashCode);
        System.out.println(hash);

        System.out.println(Integer.toBinaryString(hashCode));
        System.out.println(Integer.toBinaryString(hash));
    }

输出:

692404036
692393473
101001010001010011111101000100
101001010001010001011000000001

tableSizeFor的功能(不考虑大于最大容量的情况)是返回大于输入参数且最近的2的整数次幂的数。

    static final int tableSizeFor(int cap) {
        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;
    }

put函数

通过put函数 我们能很好的认识到HashMap的结构

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //懒加载   仅仅new  HashTable()  里面啥也没有   此处resize创建默认长度16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        if ((p = tab[i = (n - 1) & hash]) == null)//如果这个桶是空的  创建桶的第一个节点
            tab[i] = newNode(hash, key, value, null);
        else {
            //Node<K,V>这个结构就表示节点和桶首节点   e用来存储旧数据    p是当前桶的首节点
            Node<K,V> e; K k;

            /* 此时获得一个由hash值相同的元素组成的链表  链表的首节点是p
            * 可能出现两种情况
            *      ==p是树节点和p不是树节点==
            * 树:putTreeVal  红黑树插入之后返回旧数据  如果没有返回空
            * 链表:我们循环检查链表中的value和key是否equals
            *   遍历到尾部就会添加新节点  返回的e就为null
            *   插入新节点  并判断是否达到TREEIFY_THRESHOLD值
            *   if-else用来判断三种情况
            *   1. 判断p就是要找的节点
            *   2. 判断p是树节点,则调用putTreeVal方法,如果有旧数据就返回给e,否则返回null
            *   3. 此时只剩下检索链表
            */

            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//e指向旧节点
            else if (p instanceof TreeNode)
                //红黑树插入之后返回旧数据  如果没有返回空
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    /*
                    * binCount检索的是 p为首节点的第 length=bincount+2个节点 
                    * 所以下面判断的是实际节点数length>=9
                    * 比如bincount为0检索的是p.next  p是节点1   p.next就是节点2   依次类推
                    * */

                    //如果检索到链表末尾 则插入节点  并判断是否树化
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果长度大于等于9  则转变成二叉树 (注:此时两边同时加2  左边是节点数  右边是9)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果e和当前节点的key相等   则break   e记录旧节点的值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果存在旧节点  返回旧值  并更新旧节点的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;//记录旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//赋值新值
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //大于负载因子*容量的时候才扩容   故初始时put第13个扩容
        if (++size > threshold)//The next size value at which to resize (capacity * load factor).
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize函数

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //newCap=2*oldCap  扩容到原来的两倍大小
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            //当使用new HashTable(n)给定了长度  n先被转化成2的幂  然后new Node[newCap]出table的长度
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //懒加载的实现  在第一次put元素时  创建默认长度的table
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //设置下次扩容的阈值capacity*loadFactor
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {//扩容时老表不为null  如果为空跳过该片段 直接返回新建的newTab

            //此时扩容后  元素会重新迁移   在老表中的位置不代表新表中的位置
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //遍历每个桶 只有桶中有数据才需要迁移
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;

                    //如果该节点没有连接链表和红黑树
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果连接红黑树  这里红黑树额外维系了链表结构
                    //采取措施和链表类似  使用高低链表
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //连接链表
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            /*
                            * 这里非常巧妙
                            * 首先  e.hash是不变的  这里设oldCap老表长度为16
                            * 则老表的下标计算只是用了e.hash的低4位   已知e.hash&15=j
                            * 由于表扩容了一倍 所以现在  e.hash的低5位都应该被使用
                            * 而已经有了底4位使用的记录  如果e.hash的第五位是0  则数据所在位置和老表一样
                            * 如果e.hash的第五位是1   则位置为j+oldCap即j+newTab.length/2
                            * 因为如果e.hash的第五位是1  那么数据必然会存储在新表的后半段
                            * 而低四位的值就相当于offset这里是j   就就是存储位置相对于newTab.length/2的偏差
                             * */
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值