【JDK源码】HashSet、HashMap(一)

本文探讨了Java中的HashSet和HashMap。HashSet基于HashMap实现,通过HashMap的key存储元素,利用equals()和hashCode()确保不重复。HashMap的容量通常是2的幂,加载因子为0.75,put方法涉及hash计算、扩容和冲突解决。文章还提出了对put方法中hash计算、数组结构、树化和扩容底层实现的思考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashSet

HashSet实现原理

由于set是基于map实现的 这里主要介绍map

  • 是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

在这里插入图片描述

HashSet 中数据是如何保证不重复的

  • HashSet 源码分析
    通过上面的 HashSet 源码我们可以看到,在 HashSet 的构造函数中,它创建了 HashMap
    在 HashSet 的 add() 方法中是调用了 HashMap 的 put() 方法的,要 put 的 key 是要添加到 HashSet 中的元素,而value 则是一个静态常量值 PRESENT
  • 当向 HashSet 中添加元素时,如果此时添加一个 HashSet 已存在的元素时
    此时肯定会发生 Hash 冲突的
    那么接下来,会进行该元素的 equals() 方法或 == 的比较,如果它们的 equals() 方法或 == 比较的结果为 true 即相等,则 HashMap 会使用新 value 值覆盖旧 value 值
    而到此时,HashMap 的 put() 方法的返回值为一个 oldValue,故而 HashSet 的 add 方法的返回值为 false,即添加一个已存在的元素时会失败
 public boolean add(E e) {
   
   
      return map.put(e, PRESENT)==null;
 }

HashMap

  • 先看成员变量

在这里插入图片描述

//HashMap初始容量大小(16) 并且是2次幂 在我们第一次put值得时候才会给到初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子 当容量被占满0.75时就需要reSize扩容 
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表长度到8,就转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 树大小为6,就转回链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转变成树之前,还会有一次判断,只有数组长度大于 64 才会发生转换
static final int MIN_TREEIFY_CAPACITY = 64;
  • 为什么是2次幂?

(n-1)&hash等效于hash % n ,在计算元素该存放的位置的时候,用到的算法是将元素的hash与当前map长度-1进行与运算,

n-1是为了让低位都等于1,低位都为1与哈希值相与, 高位都为0与哈希值高位相与自然也等于0, 所以与出来的范围在 0-(n-1)

如果map的长度不是2的幂次,比如为15,那长度-1就是14,二进制为1110,无论与谁相与最后一位一定是0,0001,0011,0101,1001,1011,0111,1101这几个位置就永远都不能存放元素了,空间浪费相当大。也增加了添加元素是发生碰撞的机会。减慢了查询效率。所以Hashmap的大小建议是2的幂次。

在这里插入图片描述

  • 什么是加载因子?为什么是0.75

加载因子也叫扩容因子或负载因子,用来判断什么时候进行扩容的,假如加载因子是 0.5,HashMap 的初始化容量是 16,那么当 HashMap 中有 16*0.5=8 个元素时,HashMap 就会进行扩容。

那加载因子为什么是 0.75 而不是 0.5 或者 1.0 呢?

这其实是出于容量和性能之间平衡的结果:

当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生 Hash 冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低;
而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。
所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。

put方法

  • put方法是map中核心方法之一,所以一步一步分析

在这里插入图片描述

public V put(K key, V value) {
   
   
    
    return putVal(hash(key), key, value, false, true);
}
//......
static final int hash(Object key) {
   
   //根据参数,产生一个哈希值
    int h;
    //这里为什么不直接返回key.hashCode() 还要与 h>>>16来异或呢?
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//......
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
   
   
    Node<K,V>[] tab; //临时变量,存储"哈希表"——由此可见,哈希表是一个Node[]数组
    Node<K,V> p;//临时变量,用于存储从"哈希表"中获取的Node
    int n, i;//n存储哈希表长度;i存储哈希表索引

    if ((tab = table) == null || (n = tab.length) == 0)//判断当前是否还没有生成哈希表
        //这里就是默认初始化容量为16
        n 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

早上真起不来!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值