Set 怎么保证不重复

因为map中的key是不允许重复的,所以set中的元素不能重复。

HashMap的往里放元素的源码!!!

public V put(K key, V value) { 
        if (key == null) 
            return putForNullKey(value); 
        int hash = hash(key.hashCode()); 
        int i = indexFor(hash, table.length); 
        //遍历链表 
        for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
            Object k; 
            //如果key在链表中已存在,则替换为新value  HashMap 可以重复put同一个Key值不同的对象。 
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
                V oldValue = e.value; 
                e.value = value; 
                e.recordAccess(this); 
                return oldValue; 
            } 
        } 

        modCount++; 
        addEntry(hash, key, value, i); 
        return null; 
    } 

分步骤判断添加的Key值:
1、取到新添加Key值得hashCode值。
2、确定数组的index 根据Key的hashCode值和当前table的长度按位取并 h & (length-1);
按位取并,作用上相当于取模mod或者取余%。
这意味着数组下标相同,并不表示hashCode相同
3、这里的hashcode在equals前面,JVM会先判断或运算||的前部分,当这一前部分为true的时候判断终止,返回true(这是为了提高JVM的效率),所以当hashcode不同的时候,equals是不会执行的。

总结为什么Set里面不能有重复?
因为HashMap在put一个Key时会判断,将要放进去的Key的hash值与 目前HashMap中定位到的那个Key的hash值比较。
如果hash值相当,继续比较 这两个对象的地址或者内容是否相当。
如果相当:判断出来要添加的Key与HashMap中的Key重复,把Value的值给替换成最新的。
HashSet中的Value是一个固定值PRESENT。 所以修改不修改无所谓。

重写equals方法后最好重写hashCode方法,否则两个等价对象可能得到不同的hashCode,这在集合框架中使用可能产生严重后果,这样如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但不重写hashcode,那么我们再new一个新的对象,
当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致,如在存储散列集合时(如Set类),将会存储了两个值一样的对象,
导致混淆,因此,就也需要重写hashcode()

注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。如下:
(1)当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true
(2)当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false

<think>我们正在讨论Java中Set集合如何保证元素唯一性的实现原理。Set接口的常用实现类有HashSet、LinkedHashSet和TreeSet,它们各自有同的机制来确保元素重复。 1. **HashSet**:基于HashMap实现,利用HashMap的key唯一性来存储元素。 2. **TreeSet**:基于TreeMap实现,利用红黑树和比较器(Comparator)或自然顺序来保证唯一性。 3. **LinkedHashSet**:继承自HashSet,同时通过链表维护插入顺序,但唯一性保证与HashSet相同。 下面详细说明: ### 1. HashSet实现原理 HashSet内部使用HashMap来存储元素,将元素作为HashMap的key,而value则存储一个静态的Object对象(PRESENT)。当添加元素时,实际上是调用了HashMap的put方法。 ```java // HashSet的源码片段 public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { private transient HashMap<E, Object> map; private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } public boolean add(E e) { return map.put(e, PRESENT) == null; } // ... 其他方法 } ``` 在HashMap中,key是唯一的。HashMap通过计算key的哈希值(hashCode)和equals方法来判断两个key是否相等。具体过程如下: - 当向HashSet添加元素时,首先计算元素的hashCode值,根据该值确定元素在哈希表中的存储位置。 - 如果该位置没有元素,则直接添加。 - 如果该位置已有元素(或链表/红黑树),则调用equals方法比较: - 如果equals返回true,则认为元素已存在,添加; - 如果equals返回false,则将该元素添加到链表或红黑树中(在Java 8中,当链表长度超过8时,链表会转换为红黑树)。 因此,要正确使用HashSet,必须重写元素的hashCode()和equals()方法,并且保证两个相等的对象必须具有相同的hashCode。如果两个对象equals相等,则hashCode必须相等;但hashCode相等,equals一定相等(哈希冲突)。 ### 2. TreeSet实现原理 TreeSet基于TreeMap实现,利用红黑树(自平衡二叉查找树)来存储元素。TreeSet中的元素是有序的(自然顺序或指定Comparator)。TreeSet通过比较元素的大小来判断是否重复。 ```java // TreeSet的源码片段 public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable { private transient NavigableMap<E, Object> m; private static final Object PRESENT = new Object(); public TreeSet() { this(new TreeMap<>()); } // ... 其他方法 public boolean add(E e) { return m.put(e, PRESENT) == null; } } ``` 在TreeMap中,添加元素时,会通过比较器(Comparator)或者元素的自然顺序(Comparable)来比较元素。如果比较结果为0,则认为两个元素相等,会插入新元素。 注意: - 如果使用自然顺序,元素必须实现Comparable接口,并重写compareTo方法。 - 如果使用自定义比较器,则需要在创建TreeSet时传入Comparator,并重写compare方法。 因此,TreeSet判断元素是否重复的依据是:比较器(Comparator)或自然顺序(Comparable)的返回值是否为0。如果为0,则视为重复。 ### 3. LinkedHashSet实现原理 LinkedHashSet继承自HashSet,其内部使用LinkedHashMap来存储元素。LinkedHashSet在HashSet的基础上,通过链表维护了元素的插入顺序。但是,其保证元素唯一性的机制与HashSet相同,也是通过hashCode和equals方法。 ```java public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { public LinkedHashSet() { super(16, .75f, true); // 调用HashSet的特定构造方法 } } // 在HashSet中,该构造方法会创建LinkedHashMap HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } ``` ### 总结 | Set实现类 | 底层结构 | 唯一性判断依据 | 元素顺序 | |----------------|---------------|----------------------------------|--------------| | HashSet | 哈希表 | hashCode()和equals() | 无序 | | LinkedHashSet | 链表+哈希表 | hashCode()和equals() | 插入顺序 | | TreeSet | 红黑树 | Comparator或Comparable的compareTo | 自然顺序或定制顺序 | ### 注意事项 1. **HashSet和LinkedHashSet**: - 依赖hashCode()和equals()方法,因此必须正确重写这两个方法。 - 重写hashCode()时,应该保证相同的对象返回相同的hashCode,同的对象尽量返回同的hashCode(减少冲突,提高效率)。 2. **TreeSet**: - 依赖元素的比较,因此要么元素实现Comparable接口,要么提供Comparator。 - 如果同时存在Comparator和Comparable,则优先使用Comparator。 3. **性能考虑**: - HashSet的添加、删除、查找操作的时间复杂度接近$O(1)$(在哈希冲突较少的情况下)。 - TreeSet的添加、删除、查找操作的时间复杂度为$O(\log n)$,因为红黑树是平衡二叉搜索树。 ### 示例:重写hashCode和equals 以下是一个自定义类重写hashCode和equals的示例: ```java public class Person { private String name; private int age; // 构造方法、getter/setter省略 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } } ``` 这样,当两个Person对象的name和age都相等时,HashSet会认为它们是同一个对象。 ### 相关问题 1. 为什么重写equals方法时必须重写hashCode方法? 2. HashSet和TreeSet在性能上有什么区别? 3. 如何选择使用HashSet、LinkedHashSet或TreeSet? 4. 如果两个同的对象具有相同的hashCode,HashSet会如何处理? 5. TreeSet中如果两个元素通过compareTo方法比较为0,但equals方法返回false,会怎样?</think>### Java中Set集合保证元素唯一性的实现原理 Set接口的核心机制是通过**元素比较**来确保唯一性,实现类采用同策略: --- ### 1. HashSet:哈希表 + equals() [^1] ```java Set<String> set = new HashSet<>(); set.add("A"); set.add("B"); set.add("A"); // 添加失败,返回false ``` **实现原理**: - 底层基于`HashMap`实现(元素作为Key,固定Object作为Value) - 添加元素时: 1. 计算元素哈希值:`hash = hashCode()` 2. 定位桶位置:`index = hash & (n-1)` 3. 遍历桶内元素: - 若存在元素满足:`e.hash == hash && (e == key || key.equals(e))` - 则判定为重复,拒绝添加 - **时间复杂度**:$O(1)$ (理想哈希冲突情况下) **关键要求**: ```java // 必须同时重写hashCode()和equals() @Override public int hashCode() { return Objects.hash(name, age); // 相同属性组合生成相同hash } @Override public boolean equals(Object o) { // 属性值相等判断 } ``` --- ### 2. TreeSet:红黑树 + Comparator [^2] ```java Set<Integer> treeSet = new TreeSet<>(); treeSet.add(5); treeSet.add(3); treeSet.add(5); // 添加失败 ``` **实现原理**: - 底层基于`TreeMap`(红黑树结构) - 添加元素时: 1. 通过比较器判断:`comparator.compare(e1, e2) == 0 ?` 2. 返回0时判定为重复 3. 自然排序要求元素实现`Comparable`接口 - **时间复杂度**:$O(\log n)$ **比较器示例**: ```java Set<Person> set = new TreeSet<>((p1, p2) -> p1.getAge() == p2.getAge() ? 0 : 1 // 年龄相同判为重复 ); ``` --- ### 3. LinkedHashSet:链表维护插入顺序 ```java Set<String> linkedSet = new LinkedHashSet<>(); linkedSet.add("B"); linkedSet.add("A"); linkedSet.add("B"); // [B, A] 保持插入顺序且去重 ``` **实现特点**: - 继承`HashSet`,底层使用`LinkedHashMap` - 通过双向链表维护插入顺序 - **唯一性判断**与HashSet完全相同(依赖hashCode+equals) --- ### 唯一性判断对比表 | **Set实现类** | 数据结构 | 判重依据 | 顺序性 | 时间复杂度 | |----------------|---------------|----------------------------|-------------|-----------| | `HashSet` | 哈希表 | `hashCode()` + `equals()` | 无序 | $O(1)$ | | `TreeSet` | 红黑树 | `Comparator`/`Comparable` | 排序 | $O(\log n)$ | | `LinkedHashSet`| 哈希表+双向链表 | `hashCode()` + `equals()` | 插入顺序 | $O(1)$ | --- ### 核心机制深度解析 1. **哈希冲突解决**(HashSet) - 拉链法:每个哈希桶存储链表/红黑树 - 当满足以下条件时触发扩容: $$ \text{装载因子} = \frac{\text{元素数量}}{\text{桶数量}} > 0.75 $$ 2. **比较一致性要求**(TreeSet) - 必须满足:$ \text{signum}(e1.compareTo(e2)) == -\text{signum}(e2.compareTo(e1)) $ - 否则抛出`ClassCastException` 3. **并发场景处理** ```java Set<String> safeSet = Collections.synchronizedSet(new HashSet<>()); // 或使用并发集合 Set<String> concurrentSet = ConcurrentHashMap.newKeySet(); ``` --- ### 使用注意事项 1. **可变对象风险**: ```java Set<List> set = new HashSet<>(); List list = new ArrayList(); set.add(list); list.add("element"); // 修改后hashCode改变,导致内存泄漏 ``` 2. **重写规范**: - 重写`equals()`必须同时重写`hashCode()` - 需满足: $$ \forall e1,e2 : e1.equals(e2) \implies e1.hashCode() == e2.hashCode() $$ 3. **空元素处理**: - 所有Set实现允许且仅允许**一个**`null`元素 - TreeSet添加`null`会抛出`NullPointerException` --- ### 相关问题 1. 为什么重写`equals()`必须同时重写`hashCode()`? 2. `HashSet`和`TreeSet`在性能上有何差异?如何选择? 3. 如何设计一个对象使其在`HashSet`中被识别为重复? 4. `TreeSet`使用自定义比较器时需要注意哪些约束条件? 5. 为什么`LinkedHashSet`能保持插入顺序而`HashSet`能?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值