HashMap源码剖析

本文详细剖析了HashMap在JDK1.7和1.8的底层实现,包括扰动函数的作用、2的幂次方长度的原因以及从链表到红黑树的转换。JDK1.8优化了扩容策略,避免了头插法导致的死循环,通过尾插法保持链表顺序,并利用红黑树进一步提高了查找效率。
摘要由CSDN通过智能技术生成

1.HashMap源码剖析

1.1.HashMap的底层实现

1.1.1.JDK1.8之前

JDK1.8之前HashMap底层是数组和链表结合在一起使用也就是链表散列HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n - 1) & hash判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存放元素的话,就判断该元素要与存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突

所谓扰动函数指的就是HashMap的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法,换句话说使用扰动函数之后可以减少碰撞。

所谓“拉链法”就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.7的HashMap的hash方法源码。

static int hash(int h) {
   
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK1.7的hash方法的性能会稍差一点点,因为毕竟扰动了4次。

1.1.2.JDK1.8之后

相比于之前的版本,JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)(将链表转换为红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

TreeMap、TreeSet以及JDk1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

可以看看JDK1.8的hash方法,相比于JDK1.7方法更加简化,但是原理不变。

 static final int hash(Object key) {
   
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

1.1.3.HashMap的长度为什么是2的幂次方

为了能让HashMap存取高效,尽量减少碰撞,也就是要尽量把数据分配均匀。Hash值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。

这个算法为什么要这样设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

1.1.4.为什么链表长度达到8之后才开始转换为红黑树?

源码上说,为了配合使用分布良好的hashCode,树节点很少使用。并且在理想状态下,受随机分布的hashCode影响,链表中的节点遵循泊松分布,而且根据统计,链表中节点数是8的概率已经接近千分之一,而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。因为链表转换为红黑树也是需要消耗性能的,特殊情况特殊处理,为了挽回性能,权衡之下,才使用红黑树,提高性能。也就是大部分情况下,hashmap还是使用的链表,如果是理想的均匀分布,节点数不到8,hashmap就自动扩容了。为什么这么说呢,看图。

image-20220112150424139

当数组长度小于MIN_TREEIFY_CAPACITY,就会扩容,而不是直接转变为红黑树。

为啥用8?因为通常情况下,链表长度很难达到8,但是特殊情况下链表长度为8,哈希表容量又很大,造成链表性能很差的时候,只能采用红黑树提高性能,这是一种应对策略。

1.2.分析源码

1.2.1.属性

	//hash表初始化的大小为16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //hash表的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默认的加载因子为0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //链表转红黑树的结点数为8
    static final int TREEIFY_THRESHOLD = 8;

    //红黑树转链表的结点数为6(剪枝)
    static final int UNTREEIFY_THRESHOLD = 6;

    //最小的树化hash表大小为64
    static final int MIN_TREEIFY_CAPACITY = 64;

1.2.2 构造器

 
	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);
    }

    public HashMap(int initialCapacity) {
   
        this<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值