抽丝剥茧解析 HashMap 源码【看完吊打面试官】

什么是HashMap?

简单来讲,HashMap是一种常用的数据结构,由数组+链表组成+红黑树(JDK1.8之后加入的),称为“哈希表”。
在这里插入图片描述
有的文章里说是把所有Node都放到数组里,这样描述可能不好理解,可以换一个说法,Node数组中只存放链表的第一个Node节点,由于这个Node节点会有指向下一个Node的next,就相当于是把链表放到了数组中。


HashMap的组成部分:

一、 6个默认值:

1.默认初始容量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

HashMap的容量,也就是数组的大小,必须是 2 的n次幂
1 << 4 是2的4次方,等于16

2.最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

如果构造函数传入的容量值大于这个最大值,则会替换为这个最大值。
2的30次方

3.默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

作者通过概率统计学计算后得到的值,不建议修改,如果负载因子太小,导致频繁扩容,太大又增大哈希碰撞的几率

4.树化阈值

static final int TREEIFY_THRESHOLD = 8;

转化为红黑树的阈值,JDK 1.8 之后加入的,链表长度大于树化阈值,转化为红黑树,提高查询效率。

  • 链表特点:插入快,查询慢。
    – 链表查询操作的时间复杂度是 O(n)
  • 红黑树特点:查询快、占内存(一个TreeNode占用的内存是一个Node的2倍)
    – 红黑树查询时间复杂度是 O(log n)

5.红黑树退化为链表的阈值

static final int UNTREEIFY_THRESHOLD = 6;

=0.75*8 ( 负载因子 * 树化阈值)

6.最小树化容量

static final int MIN_TREEIFY_CAPACITY = 64;
  • 转化为红黑树的最小容量,容量大于64时(树化阈值的4倍),才会进行树化。
    ⭐这个是经常被忽略的点,很多人只知道链表长度达到树化阈值就会转换为红黑树,其实在树化操作之前,还会判断当前容量有没有大于64?如果没有,就先进行扩容,这又引申出一个点,扩容并非是要达到扩容阈值才会进行扩容,链表长度达到8的时候,也可能引发扩容操作。

二、2个内部类

①Node

4个属性:
  • hash:哈希值,key通过哈希算法得到的值,hash(key)
  • key:键,唯一,允许null
  • value:值,不唯一,允许null
  • Node<K,V> next; 下一个Node节点
    当哈希值相同时,(n - 1) & hash 得到相同的index,需要存储在数组中同一个位置,则存储在next
    一个接一个,形成链表
    1.7之前是头插法,由于HashMap是非线程安全,并发环境下的put操作会使链表出现环状,导致get时,next永远不为空,形成死循环。
    两个线程同时对原hash表扩容,线程一正在执行扩容(遍历单向链表),切换到线程二并在线程二完成所有扩容操作,再切换到线程一,就可能形成环状,下一次查询时造成死锁。
    1.8之后改为尾插法,避免了死锁。
重写了 hashCode() 和 equals() 方法,不重写的话,比较的是两个对象的内存地址,不是比较值

②TreeNode

红黑树的节点,→继承 LinkedHashMap.Entry<K,V> ,→ 继承 HashMap.Node<K,V>

5个属性
  • parent:父节点
  • left:左 子节点
  • right:右 子节点
  • prev:前方节点
  • red:是否是红色

构造方法

Node的构造方法

三、HashMap的5个属性

数组

transient Node<K,V>[] table;
  • HashMap容量 = table 的长度

大小

transient int size;
  • 键值对数量

修改次数

transient int modCount;
  • 迭代时如果发生了修改,抛出“并发修改异常”

下一次扩容的阈值

int threshold;
  • 容量 * 负载因子

负载因子

final float loadFactor;

四、4个构造函数

①无参构造函数

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • 无初始容量,默认负载因子0.75
  • 第一次使用时扩容,默认容量16
  • 阿里巴巴代码规范提示需要设置初始值,避免每次都是16,节省空间,大多数情况并不需要那么多

②带容量的构造函数

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  • 设置初始容量,使用默认负载因子
  • 推荐使用
  • 传入的容量值,会被二次幂算法 tableSizeFor(int cap) 转换成大于等于这个数,并且离这个数最近的一个2的n次幂,因为HashMap的容量必须是2的N次幂。
    例如:传入3,容量是4,传入5、6、7、8,容量是8,传入负数,抛出异常

③带容量+负载因子的构造函数

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;
        this.threshold = tableSizeFor(initialCapacity);
    }
  • 设置初始容量、负载因子,不使用默认值
  • 不推荐,不建议修改,如果负载因子太小,导致频繁扩容,太大又增大哈希碰撞的几率

④带Map的构造函数

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
  • 复制Map中的键值对到新的HashMap中
  • 默认负载因子0.75
  • 初始容量,根据传入的Map的size计算,得到大于等于这个size 的最近的一个2的n次幂
    假如传入的map大小为1
    1/0.75 +1 =2.333f
    小于 2的30次方,强转为int,=2
    tableSizeFor(2),=2

五、⭐2个重点方法

put

  • hash算法
    key.hashCode()的值,与自身无符号右移16位后的数进行异或运算
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//如果不判空,调用Object的hashCode方法会报异常
    }
  • put → putVal
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

在这里插入图片描述

get

getNode(hash(key), key))

查到Node节点,返回Node节点的Value,查不到返回null

  • first = tab[(n - 1) & hash]),计算数组下标,查找数组元素,得到链表的第一个Node
  • 对比key是否与传入的key相等
  • 相等 → 返回第一个Node节点
  • 不相等 → 判断第一个节点是不是红黑树
  • 是红黑树,按红黑树查找,二分查找
  • 不是,遍历循环获取next,对比key,找到key对应的Node的value,找不到,返回null

补充:

红黑树特性

1、每个节点要么是红色,要么是黑色,但根节点永远是黑色的;
2、每个红色节点的两个子节点一定都是黑色;
3、红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色);
4、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;
5、所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点);
在这里插入图片描述

  • 优点:查询速度快(优于链表、次于平衡二叉树)、增删速度快。综合性能强。
    红黑树查询时间复杂度为 O(log n)
    log : 对数,是指数函数的反函数
    链表查询时间复杂度为 O(n)
  • 缺点:占内存,所以长度低于退化阈值时需要退化为链表。
    TreeNode占的内存是Node的两倍
  • 树化阈值选择8,是因为hash分布良好的时候,链表长度大于等于8的概率很小,更多的是一种保底策略,在极端情况下哈希冲突较多导致链表长度过长的时候,保证查询效率。

位运算符

1.位移

<< 左移:不管正负,高位移出,低位补0

  • 例如:a << n ; a乘以2的n次方

>> 右移:正数高位补0,负数高位补1

  • 例如: a >> n ;a除以2的n次方

>>> 无符号右移 :不管正负,高位都补0

  • 只有无符号右移,没有无符号左移
  • 与有符号的区别:
    正数时,无区别
    负数时,带符号的高位补1,无符号的高位补0
    当移动的位数超过本身长度,则对本身长度取模,例如本身长度为3,移动5位,其实只移动了2位
2.与 &
  • 两个结果都是true,结果才是true
  • 两个二进制数对比,都是1,结果才是1,否则0
    10001
    &
    10000
    ···········
    =10000
3.或 |
  • 两个结果都是false,结果才是false
  • 两个二进制数对比,都是0,结果才是0,否则1
    1001
    |
    1000
    ···········
    =1001
4.异或 ^
  • 两个二进制数对比,相同则为0,不相同则为1
    1001
    ^
    1000
    ···········
    =0001
5.非 ~

一个二进制数,1变成0,0变成1

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值