【数据结构】HashMap1.8源码

HashMap个人笔记


关键常量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

是数组的长度。默认初始化时的容量(16),必须为2的幂次方


static final int MAXIMUM_CAPACITY = 1 << 30;

最大容量,2^30


static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认负载因子,代表什么时候会进行扩容操作(当到达链表长度的???待补充)


static final int TREEIFY_THRESHOLD = 8;

树化阈值 1,当一个桶中的链表长度超过8时会将存储结构转化为红黑树(需要同时满足树化阈值1、2)


static final int UNTREEIFY_THRESHOLD = 6;

解树化阈值,执行resize扩容操作时,当桶中的树结构节点数小于6,会将树转化为链表


static final int MIN_TREEIFY_CAPACITY = 64;

最小红黑树容量,当Map里面的数量超过这个值时,表中的桶才能进行树形化 ,否则桶内元素太多时会扩容,而不是树形化 为了避免进行扩容、树形化选择的冲突


关键变量


int threshold;

阈值,当table == {}时,该值为初始容量(初始容量默认为16);当
table被填充了,也就是为table分配内存空间后,
threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要
参考threshold。tableSizeFor的赋值属性


final float loadFactor;

负载因子(不叫加载),代表了table的填充度有多少,默认是0.75加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。

两重含义:

一是代表了当hashmap到达哪一个数据量的时候需要进行扩容操作
二是代表当前这个loadFactor值所对应的空间利用率有多少,越大代表利用率越高,但也越容易发生hash碰撞


transient Node<K,V>[] table;

table在JDK1.8中我们了解到HashMap是由数组加链表加红黑树来组成的结构其中table就是HashMap中的数组,jdk8之前数组类型是Entry<K,V>类型。从jdk1.8之后是Node<K,V>类型。只是换了个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据的。


transient int size;

size为HashMap中K-V的实时数量,不是数组table的长度


transient int modCount;

每次扩容和更改map结构的计数器




源码分析

构造函数

含义:通过调用tableSizeFor,将传入的初始容量转化为一个大于等于初始容量,且最接近初始容量的2的整次幂,再将值赋给阈值threshold

    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;
        //分析点 1
        this.threshold = tableSizeFor(initialCapacity);
    }
  • 分析:其实应该是this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;才对,但是table的初始化在第一次put的时候才初始化,threshold又会被重新赋值。猜想是与resize里面的第二个判断if (oldThr > 0) { newCap = oldThr; }有关。



tableSizeFor

含义:根据传入的初始容量,返回一个大于等于初始容量,且最接近初始容量的2的整次幂作为真正的容量

    static final int tableSizeFor(int cap) {
   
        //分析点 1
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;//分析点 2
        return (n < 0) ? 
        1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//分析点 3
    }
  • int n = cap - 1;

分情况:

① 输入值刚好是2的幂次方:例如8(1000),如果不对它进行-1操作,得到的结果是16(10000),显然是错的

② 输入值不是2的幂次方:取7或15,减不减1结果都是一样的

  • n |= n >>> 16;
  • 再右移一位也就是乘以2,这时已经是2^32,已经是负数了
  • 这个方法的目的是生成2的幂次长度的容量,不断右移可以让最高位后面的位全部变成1,最后再+1就可以保证一定是2的整次幂
  • n + 1

做完 ② 的移位操作后,最高位后面的所有位都会是1,再加1就可以得到一个2的整次幂


hash

对key的hashcode进行扰动,目的是让值更加随机,这样在put(不一定只有put)的时候取模(取余)就不会让所有的key聚集在一起,提高散列的均匀程度

    static final int hash(Object key) {
   
        int h;
        //分析点 1
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

32位的h进行右移16位,得到高16位为0,低16位为原值高16位的一串32位的数,再将两个值做异或运算(相同为0,不同为1),

  • 为什么高16位也参与?

    因为hash & (n-1)的结果只取决于低位,所以要的是低位,如果hashcode的1集中在高位的话,在put方法中取数组下标的时候就有很大概率会是全0(例如对象 A 的 hashCode 为 1000010001110001000001111000000,对象 B 的 hashCode 为 0111011100111000101000010100000。数组长度为16(16-1=15(1111)),15与A、B做&运算后结果都是0,造成哈希冲突)。所以是为了让低位的可能性更多,避免后续取模时key聚集。
  • 为什么右移16位、为什么hashcode返回32位

    因为再右移就是16乘以2等于32了,超过整型范围,变成负数了;因为int占4个字节

get

参数列表:

  • hash:对key进行hash扰动后的值
  • key:键值
    返回值列表:
  • Node<K,V>:节点
    final Node<K,V> getNode(int hash, Object key) {
   
        Node<K,V>[
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值