HashSet

HashSet

HashSet 是 Java 集合框架中的一个类,它实现了 Set 接口,基于哈希表(Hash Table)的数据结构。HashSet 是一个 无序集合,不允许存储重复元素,并且允许存储 null 值

HashSet特点

  1. 基于哈希表实现
    • HashSet 内部使用哈希表来存储元素,哈希表是一种高效的数据结构,支持快速的插入、删除和查询操作
  2. 不允许重复元素
    • HashSet 中的元素是唯一的,如果尝试添加重复元素,添加操作会被忽略。
    • 元素的唯一性是通过 equals() 和 hashCode() 方法来判断的
  3. 无序性
    • HashSet 不保证元素的顺序,元素的存储顺序可能与插入顺序不同
  4. 允许 null 元素
    • HashSet 允许存储 null 值,但只能存储一个 null
  5. 非线程安全
    • HashSet 不是线程安全的

HashSet常用方法

HashSet 实现了 Set 接口,因此支持 Set 的所有方法

HashSet底层实现

HashSet 的底层是一个 哈希表,具体来说,它内部使用了一个 HashMap 来存储元素。HashSet 的元素是 HashMap 的键(Key),而值(Value)是一个固定的 PRESENT 对象(private static final Object PRESENT = new Object())

  1. 核心字段

    • HashMap<E, Object> map
    • HashSet 内部使用 HashMap 来存储元素
    • 元素的键是 HashSet 的元素,值是一个固定的 PRESENT 对象
  2. 构造函数,HashSet 提供了多个构造函数,用于初始化内部的 HashMap

    1. 默认构造函数

      • 创建一个默认初始容量为 16,负载因子为 0.75 的 HashMap

      • HashMap 和 HashSet 在第一次创建时不会立即分配 16 的内存空间,它们的初始化是延迟加载的

      • 在第一次添加元素时(调用 put() 或 add() 方法),HashMap 会初始化一个默认容量为 16 的哈希表

        public HashSet() {
            map = new HashMap<>();
        }
        
    2. 指定初始容量的构造函数

      public HashSet(int initialCapacity) {
          map = new HashMap<>(initialCapacity);
      }
      创建一个指定初始容量的 HashMap
      
      
    3. 指定初始容量和负载因子的构造函数

      public HashSet(int initialCapacity, float loadFactor) {
          map = new HashMap<>(initialCapacity, loadFactor);
      }
      创建一个指定初始容量和负载因子的 HashMap
      
    4. 基于集合的构造函数

      • 用于通过已有集合初始化新的 HashSet 实例。该构造函数会将传入集合 c 的所有元素添加到新 HashSet 中,并自动去重
      • 根据集合 c 的容量初始化 HashMap 的大小,具体如下
      public HashSet(Collection<? extends E> c) {
          map = new HashMap<>(Math.max((int) (c.size() / .75f) + 1, 16));
          addAll(c);
      }
      
      
  3. 哈希函数

    • HashMap 使用键的 hashCode() 方法计算哈希值
    • 哈希值通过扰动函数((h = key.hashCode()) ^ (h >>> 16))进一步处理,以减少哈希冲突
  4. 哈希冲突解决

    • 当两个键的哈希值映射到同一个桶(Bucket)时,会发生哈希冲突
    • HashMap 使用链表或红黑树来解决哈希冲突
    • 如果桶中的元素数量较少(小于 8),使用链表存储。
    • 如果桶中的元素数量较多(大于等于 8),如果还满足 哈希表总容量 ≥ 64,则将链表转换为红黑树
    • 当红黑树中的节点数 减少至 6 或以下 时,树会退化为链表以节省内存。此阈值与树化阈值(8)之间设置 2 的差值,避免频繁转换
    • 退化操作 仅由节点数触发,与哈希表的当前容量无关

HashSet扩容机制

  1. 扩容的触发条件

    • 当哈希表中的元素数量超过 容量 × 负载因子 时,HashSet 会自动扩容
    • 是元素数量不是Node数组中的数量,即使是全部在一条链表上也会进行扩容
    • 容量:哈希表的当前大小(桶的数量)
    • 负载因子:哈希表在扩容之前可以达到的填充比例,默认值为 0.75
  2. 扩容的核心逻辑

    • 新容量通常是旧容量的 2 倍
    • 新阈值是新容量 × 负载因子
    • 创建一个新的哈希表,容量为新容量
    • 扩容需要遍历旧表中的所有元素,并将其重新分配到新表中
    • 扩容会创建一个新的哈希表,容量为旧表的 2 倍

    迁移数据

    • 遍历旧表中的每个桶,将元素重新分配到新表中。
    • 如果桶中只有一个元素,则直接放入新表。
    • 如果桶中是链表,则将链表拆分为低位链表和高位链表,分别放入新表。
    • 如果桶中是红黑树,需要将红黑树拆分为两棵红黑树,分别放入新表中的不同位置

HashSet判断元素是否重复

  1. 调用元素的 hashCode() 方法,生成初始哈希值,HashMap内部对哈希值进行二次计算(如异或高位),减少哈希冲突概率

  2. 根据扰动后的哈希值,通过 (n-1) & hash 公式确定元素应存入的哈希桶

  3. 若目标桶已有元素(哈希冲突),遍历该桶内的链表或红黑树节点

  4. 进一步通过 ==(引用地址相同)或 equals()(内容相同)判断是否重复

  5. 满足以下任一条件即判定为重复元素

    • 哈希值相同,且引用地址相同(== 返回 true)

    • 哈希值相同,且 equals() 方法返回 true

      if (e.hash == hash &&
          ((k = e.key) == key || (key != null && key.equals(k))))
          break;
      
  6. 若未找到重复元素,将新元素插入链表尾部或红黑树对应位置

如何正确的重写equals()和hashCode()方法

  1. equals() 方法的重写规则

    • 自反性:x.equals(x) 必须返回 true。
    • 对称性:若 x.equals(y) 为 true,则 y.equals(x) 也必须为 true。
    • 传递性:若 x.equals(y) 和 y.equals(z) 为 true,则 x.equals(z) 必须为 true。
    • 一致性:多次调用 equals() 结果应一致(对象未被修改时)。
    • 非空性:x.equals(null) 必须返回 false

    实现步骤

    • 类型检查:使用 if (o == null || getClass() != o.getClass()) 判断对象类型是否一致。
    • 字段比较:逐一比较所有关键字段(影响对象逻辑相等的字段)。
    • 空安全处理:使用 Objects.equals(field, other.field) 替代 field.equals() 以避免空指针
    @Override 
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass())  return false;
        MyClass obj = (MyClass) o;
        return Objects.equals(field1,  obj.field1)  
            && Objects.equals(field2,  obj.field2); 
    }
    
  2. hashCode() 方法的重写规则

    核心原则

    • 一致性:若 equals() 结果为 true,则两个对象的 hashCode() 必须相同。
    • 高效性:哈希码应尽量分布均匀以减少哈希碰撞。
    • 包含所有关键字段:哈希码需基于 equals() 中比较的所有字段生成

    生成哈希码的方法

    • Java 7+:使用 Objects.hash(field1, field2, ...)
    @Override 
    public int hashCode() {
        return Objects.hash(field1,  field2);
    }
    
    • 数组字段处理:使用 Arrays.hashCode() 生成数组的哈希码
  3. 注意事项

    • 同时重写 equals() 和 hashCode(),若只重写 equals() 而忽略 hashCode(),可能导致哈希集合(如 HashMap)行为异常,例如无法正确检索对象

    • 继承场景处理,若父类已重写 equals()/hashCode(),子类需调用 super.equals() 和 super.hashCode() ,并追加子类字段的比较

    • 如果两个子类的字段相等,但是父类不同,如果不调用父类的两个方法则会错误的认为两个子类对象相等

    • 避免可变字段参与哈希码,若字段可变且参与哈希计算,对象存入集合后修改字段会导致哈希码变化,破坏集合的稳定性

      • 当向 HashSet 中添加元素时,会先计算对象的哈希值(通过 hashCode() 方法),然后根据哈希值将对象存储到对应的位置,调用 equals() 方法确认对象是否存在

      • 当从 HashSet 中删除元素时,会先计算对象的哈希值,然后根据哈希值找到对象的位置,最后调用 equals() 方法确认对象是否匹配

      • 对象的哈希码是基于字段的值计算的。如果字段的值被修改,哈希码也会随之变化

        class Person {
            int id; // 可变字段
            String name;
        
            @Override
            public int hashCode() {
                return id; // 哈希码基于 id 计算
            }
        }
        
        HashSet<Person> set = new HashSet<>();
        Person p = new Person();
        p.id = 1;
        set.add(p); // 哈希码为 1,存储在位置 1
        p.id = 2;  // 哈希码变为 2
        set.contains(p); // 集合会去位置 2 查找,但对象实际存储在位置 1,导致查找失败
        
      • 其中添加删除HashSet中的对象也类似,导致集合的稳定性被破坏

        • 无法删除对象:调用 remove() 方法时,集合无法找到对象的位置,导致删除失败
        • 重复存储对象:如果修改字段后再次添加对象,集合会将其视为一个新对象,导致重复存储
        • 数据不一致:集合的内部状态被破坏,可能导致不可预知的错误
      • 解决方法

        • 避免可变字段参与哈希码计算:确保参与哈希码计算的字段是不可变的,以维护集合的稳定性
        • 先删除再修改:如果需要修改对象的字段,可以先从集合中删除对象,修改后再重新添加
posted @   QAQ001  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示