java集合之HashMap源码分析(常用函数,扩容,哈希冲突,线程不安全问题,HashSet)

本文详细分析了Java中的HashMap,包括成员变量、构造函数、哈希冲突的产生与解决方法,如哈希函数的设计、桶数组长度为2^n的原因、使用&而非%的原因。此外,还探讨了HashMap在解决哈希冲突时如何从链表转换为红黑树,以及put和get方法的实现。同时,文章提到了HashSet的实现原理及其add和contains方法。最后总结了HashMap在处理哈希冲突、构造函数、hash函数和哈希桶数组下标计算等方面的关键点。
摘要由CSDN通过智能技术生成

HashMap基础

HashMap的成员变量

静态变量

1.哈希桶数组的默认长度为16,同时其长度一定要为2^n,默认负载系数为0.75,其最大长度为2的30次方。

2.链表树化的条件是:哈希桶数组的长度大于等于64且链表中节点的个数大于等于8

3.红黑树链表化的条件是:树中节点数小于等于6。

  //哈希桶数组的默认长度(16)二进制:10000。
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
  //哈希桶数组的最大长度(2^30)
  static final int MAXIMUM_CAPACITY = 1 << 30;
  //默认的负载系数
  static final float DEFAULT_LOAD_FACTOR = 0.75f;
  //树化的最小链表节点数(链表节点数要大于等于8且哈希桶数组的长度大于等于64)
  static final int TREEIFY_THRESHOLD = 8;
  //由树转为链表的节点数,当树中节点数小于该值会转换为链表。
  static final int UNTREEIFY_THRESHOLD = 6;
  //树化的最小哈希桶数组值
  static final int MIN_TREEIFY_CAPACITY = 64;

实例变量

1.哈希桶数组存放的数据类型可以是链式节点(Node),也可以是树式节点(TreeNode继承自Node)。

2.threshold是桶扩充的阈值,这个阈值等于capacity * loadfactor,当键值对的数量超过这个阈值时会扩容。

  //哈希桶数组
  transient Node<K,V>[] table; 
  //存储键值对的Set,存储的类为Map中的内部类    
  transient Set<Map.Entry<K,V>> entrySet;                    
  //键值对的数量
  transient int size;
  //HashMap结构修改的次数
  transient int modCount;
  //扩容的阀值,当键值对的数量超过这个阀值会产生扩容
  int threshold;
  //负载因子
  final float loadFactor;

链式节点

这是链式节点的存储结构,其实是实现了内部接口Entry<K,V>。树式节点继承自链式节点,这里不写出来了。

static class Node<K,V> implements Map.Entry<K,V> {
   
        final int hash;
        final K key;
        V value;
        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;
        }

        public final K getKey()        {
    return key; }
        public final V getValue()      {
    return value; }
        public final String toString() {
    return 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;
        }

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

HashMap的构造函数

无参数构造函数:构造一个默认容量(16)与默认负载系数(0.75)的HashMap。

 	/**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
	public HashMap() {
   
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

传入容量的构造函数,与无参构造函数类似,会将传入的容量转化为大于该值的最小的2的幂次方,并赋值给扩充阈值。

public HashMap(int initialCapacity) {
   
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

传入容量和负载系数的构造函数,这里的tableSizeFor方法可以理解为将传入的容量转化为大于该值的最小的2的幂次方,比如传入6,就会返回8。可以看到HashMap没有在构造函数中初始化hash桶数组

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

HashMap减少哈希冲突与解决哈希冲突的方法

哈希冲突

比如现在有这几个key值:5,28,19,15,20,33,12,17,10,而我们的哈希桶数组大小是9,哈希函数为了简便起见为:

H(key)=key%9

会发现H(28)=1,而H(19)=1,这样就产生了哈希冲突。解决哈希冲突就是HashMap中要做的事情。

HashMap的hash函数与哈希桶数组下标的计算(重要)

HashMap是如何计算key的hash值呢,是通过hash函数。这里还可以知道HashMap存储的键值对允许key值为null。

 static final int hash(Object key) {
   
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

之后计算对应哈希桶数组的下标是通过这行代码,其中hash是通过上面的hash函数求出来的哈希值。

&是按位与
i=(n - 1) & hash

为什么要无符号右移16位后做异或运算(减少哈希冲突)

第一次看到这里我也是很奇怪为什么hash函数不直接使用key的hashCode,而是使用下面的这行代码。

>>>是无符号右移
^是异或
(h = key.hashCode()) ^ (h >>> 16);

可以理解为设计者想要将高低二进制特征混合,防止哈希桶数组长度较小时,哈希桶数组下标的计算结果只与哈希值的低16位有关,而造成哈希冲突

以图中例子可知,h右移16位,相当于把高16位的数移动到了后11位,而在于h自身做异或操作后,原本高16位没有变化,低16位就变成了低16位与高16位的异或结果,这样可以将高低位二进制特征混合起来
在这里插入图片描述
而之所以要在低16位的结果改为高低位混合的原因在于,哈希桶数组的长度往往不会很大,也就是说除非长度大于2^16+1,才会用到高16位。这样会造成的结果是:如果两个hash值在低16位没有差别,而差别在高16位,如果低16位结果没有改变的话,他们计算出的哈希桶数组下标就相同了,很容易出现哈希冲突。而HashMap这种设计方法就是为了减少哈希冲突。

计算下标的方法:
i=(n - 1) & hash

这就是一个n值为16的例子,可以看到高16位的二进制特征都丢失了。
在这里插入图片描述

为什么桶数组的长度是2^n(减少哈希冲突)

是为了让结果更加均匀。比如哈希桶数组的长度为17,那样17-1的结果就是16(00010000),可以看到,因为是与运算,最后计算出的下标值就只与hash值的第五位有关系了,其他值不管为0或1与运算之后都是0,这样会造成更大的哈希冲突。

而如果桶数组的长度为2^n,做完减1操作后,其二进制就有多个1(16-1=15(00001111)),相当于结果与hash值多个位置有关(所有二进制为1的位置),可以有效地减少哈希冲突。
在这里插入图片描述

为什么使用&而非%(节省时间)

其实理论上来说两种方式结果相同,不过按位与的操作会更快。因为%是算术运算,最终还是会转换为位运算、

HashMap解决哈希冲突的方法(重要)

HashMap的常规存储方式是数组,数组中存放 Node<K,V>的节点,但是为了防止出现哈希冲突,HashMap使用数组+链表+红黑树的存储方式。

HashMap理想的情况是不出现哈希冲突,一个桶中装一个值。但不幸的是,即使HashMap已经通过上述很多种方式减少哈希冲突,可是哈希冲突还是会出现。HashMap中使用链表红黑树来解决哈希冲突。
在这里插入图片描述
HashMap会在链表的长度大于等于8且哈希桶数组的长度大于等于64时,会将链表树化。红黑树是一个自平衡的二叉查找树,因此查找效率就会从O(n)变成O(logn)。

static final int TREEIFY_THRESHOLD = 8;
static 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值