详解:Set集合是如何保证元素不重复的

278 阅读3分钟

Set集合通过以下机制确保元素不重复:

一、Set接口的设计原则

Set接口继承自Collection,强制要求实现类不允许包含重复元素。其核心约束为:

  • 唯一性:每个元素在Set中只能出现一次。
  • 无索引:不提供基于索引的访问方法(如get(int index))。

二、实现类机制解析

不同的Set实现类通过不同方式确保元素唯一性:

1. HashSet(基于哈希表)

  • 底层结构:基于HashMap实现,元素作为HashMap的键存储。

  • 唯一性判断

    • 哈希码(hashCode):调用元素的hashCode()方法确定存储位置。
    • 相等性(equals):哈希冲突时,调用equals()方法比较元素内容。
  • 添加流程

    1. 计算元素哈希值,定位到哈希桶。
    2. 若桶为空,直接存入。
    3. 若桶非空,遍历链表/红黑树,通过equals()比较是否存在重复元素。
    4. 若存在重复,丢弃新元素;否则插入链表/树。
  • 代码示例

    public boolean add(E e) {
        return map.put(e, PRESENT) == null; // HashMap的键唯一
    }
    

2. TreeSet(基于红黑树)

  • 底层结构:基于TreeMap实现,元素按自然顺序或Comparator排序。

  • 唯一性判断

    • 比较结果(compareTo/compare):通过ComparableComparator比较元素。
    • 相等性定义:当compareTo()compare()返回0时,视为重复。
  • 添加流程

    1. 通过比较器确定元素位置。
    2. 若比较结果为0,替换或丢弃元素(根据实现逻辑)。
    3. 若比较结果非0,插入到合适位置以维持排序。
  • 注意事项

    • 与equals()的一致性:若compareTo()equals()逻辑不一致,可能导致元素被视为重复但equals返回false。

3. LinkedHashSet(有序哈希集合)

  • 底层结构:继承自HashSet,通过双向链表维护插入顺序。
  • 唯一性判断:与HashSet相同(依赖hashCode和equals)。

三、关键方法的作用

  1. hashCode()

    • 确定元素在哈希表中的存储位置。
    • 若哈希码冲突,需进一步通过equals()判断是否重复。
  2. equals()

    • 精确判断两个对象是否逻辑相等。
    • 必须满足自反性、对称性、传递性和一致性。
  3. compareTo() / compare()

    • 在TreeSet中定义元素的排序和唯一性。
    • 返回0时视为重复,无论equals()结果如何。

四、用户自定义类的注意事项

  1. 重写hashCode()和equals()

    • 规则:若a.equals(b)为true,则a.hashCode() == b.hashCode()必须成立。

    • 示例

      public class Person {
          private String name;
          private int age;
      
          @Override
          public int hashCode() {
              return Objects.hash(name, age); // 基于属性生成哈希码
          }
      
          @Override
          public boolean equals(Object obj) {
              if (this == obj) return true;
              if (obj == null || getClass() != obj.getClass()) return false;
              Person person = (Person) obj;
              return age == person.age && Objects.equals(name, person.name);
          }
      }
      
  2. 实现Comparable或提供Comparator

    • TreeSet要求:元素必须可比较,否则抛出ClassCastException

    • 示例

      // 自然排序
      public class Person implements Comparable<Person> {
          @Override
          public int compareTo(Person other) {
              return this.name.compareTo(other.name);
          }
      }
      
      // 定制Comparator
      TreeSet<Person> set = new TreeSet<>(Comparator.comparingInt(Person::getAge));
      

五、常见问题与解决方案

问题场景原因解决方案
HashSet添加重复元素未正确重写hashCode和equals方法检查并正确实现hashCode()和equals()
TreeSet抛出ClassCastException元素未实现Comparable且无Comparator提供Comparator或实现Comparable接口
比较逻辑与equals不一致compareTo()与equals()结果冲突确保两者逻辑一致

六、总结

  • HashSet:依赖哈希表和equals()/hashCode(),适合快速查找。
  • TreeSet:依赖红黑树和比较器,适合有序遍历。
  • LinkedHashSet:在HashSet基础上维护插入顺序。
  • 核心原则:正确实现元素的哈希码、相等性及比较逻辑,确保Set正确识别重复元素。

七、注意事项

  1. 哈希冲突≠重复元素:哈希值相同但内容不同的对象会被放到同一桶(链表或树中)。
  2. 不可变对象更安全:若对象存入Set后发生修改,可能导致哈希值变化,引发内存泄漏或重复元素。
  3. 线程不安全:多线程操作需用Collections.synchronizedSet()ConcurrentHashMap.newKeySet()包装。

更多分享

  1. 一文带你吃透Android中常见的高效数据结构
  2. 详解:ArrayMap和SparseArray在HashMap上面的改进
  3. 详解:HashMap与TreeMap、HashTable的区别
  4. 详解:LinkedHashMap的工作原理和实现
  5. 一文带你搞懂HashSet和TreeSet的区别