【集合-HashMap】源码级理解HashMap之深入构造方法和神奇位运算

HashMap系列文章(传送门):

一:源码级理解HashMap之深入构造方法和神奇位运算

二:源码级理解HashMap之resize()方法,带你一行行手撕

三:源码级理解HashMap之putVal()方法,一行行手撕源码

四:源码级理解HashMap之get()和remove()方法

壹:HashMap的基本属性

//序列化ID
private static final long serialVersionUID = 362498820763181265L;
//哈希表默认的桶数,也就是数组的长度,同时这个容量必须是二的次数幂
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//桶的最大数量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化的阈值
static final int TREEIFY_THRESHOLD = 8;
//从树化变回链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//树化的另一个控制参数,最小的桶数
//所以树化:链表个数大于8,同时桶数不小于64
static final int MIN_TREEIFY_CAPACITY = 64;
//数组+链表中的数组
transient Node<K,V>[] table;
//threshold表示当HashMap的size大于threshold时会执行resize操作,就是扩容阈值 
int threshold;
//当前key-value键值对个数
transient int size;
//结构修改时会自增,用于fast-fail机制
transient int modCount;


//节点内部类,Node相关属性和方法
static class Node<K,V> implements Map.Entry<K,V> {
		//Node的hash值
        final int hash;
        //键值key
        final K key;
        //value值
        V value;
        //链表的next节点
        Node<K,V> next;
		//构造方法,很简单,过
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
		//get,set,toString过
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
		//计算hash值的算法,底层调用Object.hashCode()的native方法,
		//说明底层由c++实现,这里就是返回key的哈希与value的哈希异或的值
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
		//设置新值,并返回老值
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
		//重写equals方法
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

看到这里你可能会有几个疑惑?

这里会先给出结论,原理我们会在后文中一点点揭开面纱

1.为什么桶的长度必须是2的整数次幂?

  • 在后文中查找hash地址的时候,会进行取模运算,如果利用位运算进行取模运算必须保证长度为2的整数次幂

2.table的默认值为null,到底是什么时候初始化的?

  • 使用懒加载机制,当第一次put()时才会初始化,防止你new出来一个HashMap你不用,造成的内存浪费,在集合中懒加载机制十分常见

3.为什么要将链表树化?为什么又变回来?

  • 当链表太长时,遍历又变成了链表的顺序查找,效率低下,变成红黑树可以加速查找速率(红黑树后期带你手撕),又变回来是因为链表长度缩短后,没必要使用红黑树,顺序查找足矣

4.什么是负载因子呢?

  • 比如说当前的容器容量是16,负载因子是0.75,16*0.75=12,也就是说,当容量达到了12的时候就会进行扩容操作,也就是resize()方法,负载因子是根据时间和空间权衡下经大量实验得到的,所以一般我们不传入负载因子

贰:构造方法

  • 双参构造器:指定HashMap的初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
		//检测initialCapacity和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;
        //tableSizefor()方法稍后讲解
        //这里就是根据传入的初始容量返回扩容阈值(必须是二的次数幂大小)
        this.threshold = tableSizeFor(initialCapacity);
    }
  • 指定初始容量创建HashMap

套娃,调用双惨构造器,负载因子传入DEFAULT_LOAD_FACTOR默认负载因子

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • 空参构造器

传入默认负载因子0.75,其他字段都是默认值

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • 传入集合构造器

这里涉及到put操作,我们在后文中再来解释

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

叁:神奇的异或操作

  • tableSizeFor()方法,根据传入容量返回一个>=cap的最小二的整数次幂的数
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;
    }

如何实现,这里结合一个栗子cap=10画图给你解释:(这里基本的位运算就不解释了)

首先我们明白一点,2的整数次幂的数,最高位是1,其余位都是0,例如16的二进制位:10000,那么15就是1111,所以反复或的操作就是不断地把每个位数都变成1,最后+1就变成了最近的二的整数次幂的数

在这里插入图片描述
第一次或操作:向右移动一位,最高有效位1也会向右移动一位,我们知道或操作有1就变成1,所以最高两位有效位都会变成1
在这里插入图片描述
第二次或操作:当再向右移动两位,相当于把第一步的高两位向后移动两位,所以或运算后,四位都是1

此时已经完成操作了,再移动结果不改变,最多移动16位,是因为int4个子节,32位,最多移动高16位到低16位就能完成操作

所以当位运算操作完之后,(n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;这里如果不超过最大值就返回n+1,例子中返回15+1 = 16,所以这就是sizeForTable()的算法

  • 再介绍后文中会使用到的位运算:取模运算(n - 1) & hash

常用的hash函数有:直接定址,数字分析,等等(数据结构应该都学过),HashMap中使用的是初留取余法,就是取模运算,再HashMap底层使用了位运算实现取模操作,即(n - 1) & hash = hash % n,n表示桶也就是数组的长度

位运算实现取模操作有个前提,n必须是二的整数次幂,也就回答了上文的问题

同样,我用一个栗子结合图告诉你原理:a%b=a&(b-1)假设a=10,b=8

在这里插入图片描述
所以我们想取余数,就是得到移出来的三位,也就是原来1010的后三位,所以接下来求后三位,首先我们先知道一个特性:一个数和1做与运算都是本身:0&1=0,1&1=1,所以我们将b,也就是一个n位2的整数次幂的数-1就会得到n-1位全1的数,例如16:10000,15:1111

结合这两个特性,就可以得到余数
在这里插入图片描述

  • Hash()算法中的位运算
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这里hash函数根据key获取散列地址,h = key.hashCode()) ^ (h >>> 16操作意义在哪?

上面的方法中,我们可以看到hash值会和长度做位运算,当数组的长度很短时,只有低位数的hashcode值能参与运算。而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率。并且使得高16位和低16位的信息都被保留了。

大家可以打开hashmap的源码,找到hash方法,按住ctrl点击方法里的hashcode,跳转到Object类,然后可以看到hashcode的数据类型是int。int为4个字节,1个字节8个比特位,就是32个比特位,所以16是因为32对半的结果,也就是让高的那一半也来参与运算

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值