HashMap源码学习(链表部分)

红黑树学习笔记:红黑树学习

HashMap 介绍

hashmap支持null键值
jdk1.8前hashmap由数组加链表组成,数组是hashmap主体,链表是为了解决hash冲突。
jdk1.8之后引入了红黑树,当链表长度大于阈值(默认为8)并且当前数组长度大于64时,此时索引位置上的数组会转化为红黑树,但是如果阈值大于8,但是数组长度小于64,并不会转换成红黑树,而是进行数组扩容。(阈值大于8且长度大于64后转红黑树效率会变快)

HashMap存储逻辑

根据key值的hashCode()值进行无符号右移(>>>),按位异或(^),按位与(&)计算出索引。如果索引处已经有值,判断两个key的hashCode()是否相等,不相等则在该索引创建链表向下存储,如果hash值相等,则发生了hash碰撞,需要判断两个key的值是否相同如果相同则覆盖,不相同则顺着链表依次向下执行以上逻辑。

1.HashMap中Hash函数怎么实现的?
对key的hashCode值做hash操作,无符号右移然后做异或运算。
还有伪随机数法和取余数法,位运算是效率最高的
2.HashCode值相等怎么办?
判断两个key的值是否相等,如果相等则覆盖value,否则顺着链表向下依次对比,如果链表长度超过8且数组长度超过64则转为红黑树
3.HashMap扩容逻辑?
当存储的元素个数超出临界值时进行扩容,长度为原数组的2倍,并且复制原来的数组。
4.为什么要添加红黑树?
当存储数据量较大时,可能一个索引下存储了很多个数据,这些数据都在一个链表上,此时读取数据的时间复杂度是O(n),而如果转换成红黑树,那么时间复杂度为O(logn),读取效率变高。当红黑树内元素被删除的少于6位后,会重新变成链表。
5.为什么HashMap的长度为啥必须是2的n次幂
为了减少hash碰撞。为了让hashMap存储高效,最好能把数据比较均匀的存放在每一个索引上,而比较好的办法就是取余,hash%length,而hash%length==hash&(length-1)(前提是length=2的n次幂)
6.怎么做到长度自动变为2的n次幂的?
数组长度一定是2的n次幂,如果指定长度不是2的n次幂,则通过无符号右移,按位或运算,算出大于指定值的最小的2的n次幂
7.为什么当链表长度超过8才转红黑树?
因为在随机哈希值的概率下,理论上随机到每个桶的概率是满足泊松分布的。泊松分布理论中,一个链表长度达到8的概率为0.00000006,而又因为红黑树虽然查询效率高,但是占用的空间是链表的2倍,所以其实java还是不太希望链表的长度转化为红黑树的,因此阈值设置为8 。
8.为什么链表长度<=6之后变回链表?
变回链表是因为链表长度小于6的时候和红黑树的查询速度差距是很小的,而设置为6的原因是防止频繁切换链表和红黑树,比如如果小于8就转换,则如果红黑树删除一个元素变为7,转换为链表,如果这时又添加了一个元素,则又变为红黑树,影响性能,因此中间长度为7做过度。

HashMap比较重要的常量

  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认数组长度
  2. static final int MAXIMUM_CAPACITY = 1 << 30; //最大数组长度
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子
  4. static final int TREEIFY_THRESHOLD = 8; //链表转换为红黑树的阈值
  5. static final int UNTREEIFY_THRESHOLD = 6; //红黑树转换为链表的阈值
  6. static final int MIN_TREEIFY_CAPACITY = 64; //数组长度大于这个值链表才 (要同时满足MIN_TREEIFY_CAPACITY和TREEIFY_THRESHOLD)
  7. int threshold; //边界值,当数组内的元素大于该值就扩容,=数组长度*负载因子

HashMap构造方法

public HashMap(int initialCapacity, float loadFactor) {
		//initialCapacity指定长度,loadFactor负载因子默认0.75
        if (initialCapacity < 0)
        	//长度小于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;
        //计算大于指定长度的最小的2的n次幂
        this.threshold = tableSizeFor(initialCapacity);
    }
//返回大于指定长度的最小的2的n次幂
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;
    }

this.threshold = tableSizeFor(initialCapacity);构造方法中直接将计算的数组长度赋值给了边界值字段,这是因为jdk1.8之后不在构造方法中初始化table了,而是推迟到put方法中初始化table,会在table中重新计算边界值

成员方法

  • put
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(h = key.hashCode()) ^ (h >>> 16);
目的是为了减少hash碰撞,增加性能。
向右移16位做异或运算,是为了保留高16位的特征,如果直接与,那么高16位的特征大概率直接丢失,而先向右移16位做异或运算,可以将低16位和高16位的特征进行混合,从而能更好的保留高位和地位的特征。
如果是用|或者&?
如果是|,则运算结果会偏向1.
如果是&,运算结果会偏向0.

HashMap扩容

结论:jdk8之后,HashMap扩容不需要rehash,原数组中的元素位置要么还是原索引,要么是索引+原数组长度。比如数组长度16,两个元素索引都为5,那么扩容之后,如果元素的bit以0开头 则位置不变,以1开头索引变为5+16=21

链表部分扩容源码解析

						//Node<K,V> loHead = null, loTail = null 低位链,即和旧数组长度进行与运算为0的,在新数组中索引不变。
                        //Node<K,V> hiHead = null, hiTail = null 高位链,hash值和旧数组长度进行与运算结果不为0,在新数组中下标=旧索引+旧数组长度。
                        //原理:
                        //旧数组长度为16,设下标为15的桶中有n个元素,那么这些元素的hash值后4位一定都是1111
                        //因为索引是根据(length-1)& hash 计算得出的,15的二进制为1111,与运算是有0为0,那么hash值和((length-1)=1111)进行与运算得出1111,hash的后四位也只能是1111。
                        //而扩容之后,此链表内的元素重新计算索引,要么是原位置不变,要么是原索引+原数组长度。
                        //例:
                        //hash后5位->:            11111
                        //(newLength-1) = 31 ->    11111
                        //与运算结果->             11111 -> 31
                        //
                        //hash后5位->:            01111
                        //(newLength-1) = 31 ->    11111
                        //与运算结果->             01111 -> 15
                        //从上面运算可以得出       扩容后的链表索引如果hash第1位是0则在原位,是1则等于原索引+旧索引长度
                        //而源码中是和旧数组长度进行与运算,如下
                        //(e.hash & oldCap),hash值和旧数组长度进行与运算,旧数组为16->10000,那么hash值只有后5位会参与运算,而后4位都是1,那么与运算的结果只能是00000或者10000
                        //  ?1111
                        //& 10000
                        //  ?0000 ?=0或1
                        //可以得出 当(e.hash & oldCap)=0,则说明hash值后五位的首位一定是0,否则是1,再根据上面的规律,那么首位为0 则属于低位链,为1则属于高位链。
                        Node<K, V> loHead = null, loTail = null;
                        Node<K, V> hiHead = null, hiTail = null;
                        Node<K, V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                //这部分代码困惑了我好几天,现在终于明白了。
                                //第一次进来loTail = null , loHead = e , loTail = e 此时 loHead=loTail
                                //第二次进来 loTail.next = e,那么loHead.next=e  ,然后 loTail = e , 此时 loTail指向的位置是loTail的子节点,即loTail=loHead.next
                                //第三次进来 loTail指向的是loHead的子节点, loTail.next = e ,那么就相当于loHead.next.next=e , loTail = e , 此时 loTail又指向loHead.next.next
                                //以此类推,loTail一直指向的是loHead的尾结点。
                                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。
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值