
Set 家族全是 Map 的“只存 key 版”Set的核心承诺是元素不重复。但如果你读HashSet源码会发现一件很有意思的事——它自己几乎不存数据去重逻辑全靠一个HashMap。这一篇揭开Set家族借 Map 上位的秘密顺便把LinkedHashSet和TreeSet的底层也一并讲清。一、HashSet 的真相包装了一个 HashMap看HashSet的字段和构造器就明白了privatetransientHashMapE,Objectmap;privatestaticfinalObjectPRESENTnewObject();// 占位 valuepublicHashSet(){mapnewHashMap();}HashSet把元素当作HashMap的key存进去value 统一用一个静态空对象PRESENT占位。add方法publicbooleanadd(Ee){returnmap.put(e,PRESENT)null;// key 已存在则 put 返回旧 value非 null→ false}put返回null说明这个 key 之前不存在add 成功返回 true如果 key 已存在put返回PRESENT非 nulladd 返回 false——去重就是靠 HashMap 的 key 不可重复实现的。所以HashSet的元素就是map.keySet()。二、去重的契约equals hashCode既然底层是 HashMap 的 key那HashSet判断两个元素是不是同一个用的就是 HashMap 那套先比 hashCode再比 equals见 HashMap 篇。所以把自定义对象放进 HashSet必须正确重写equals和hashCode否则只重写equals不重写hashCode两个逻辑相等的对象可能 hashCode 不同 → 落到不同桶 → 都被加进去去重失效。只重写hashCode不重写equalshashCode 相同但 equals 不同 → 挂在同一个桶的链表上被认为不同元素照样去重失效。经典反例你new User(张三)两次如果User没重写这两个方法HashSet 会当它俩是不同的人。重写后才会判定重复、只留一个。三、性能add/contains/remove 都是 O(1)因为直接委托给 HashMapHashSet的增、查、删平均复杂度都是 O(1)。这是它相对于手写 List 遍历 contains的最大优势——后者是 O(n)。四、LinkedHashSet保插入序的 SetLinkedHashSet是HashSet的子类构造时建的是LinkedHashMapHashSet(intinitialCapacity,floatloadFactor,booleandummy){mapnewLinkedHashMap(initialCapacity,loadFactor);}所以它 HashSet 的去重能力 LinkedHashMap 的插入顺序。遍历时按你 add 的顺序输出而不是 HashSet 那种无序。代价同样是节点多两个引用、内存略大。适合要去重、又想按添加顺序展示的场景比如去重后的用户操作流水。五、TreeSet排序的 SetTreeSet底层是TreeMap元素当作 key同样用PRESENT占位publicTreeSet(){this(newTreeMapE,Object());}特性元素自动排序自然序或构造时传Comparator。元素必须可比较否则add抛ClassCastException不允许 null除非比较器允许。增删查 O(log n)。实现NavigableSet支持first()、last()、lower()、higher()、subSet()等范围/邻近操作。适合要去重且要排序/取区间的场景比如排行榜去重后取 Top N。六、三兄弟怎么选需求选谁去重不关心顺序要最快HashSet去重 按插入顺序遍历LinkedHashSet去重 排序 范围查询TreeSet对应底层一目了然HashSet→HashMapLinkedHashSet→LinkedHashMapTreeSet→TreeMap。整个 Set 家族就是 Map 家族的只存 key 版记住这层关系Set 的行为就全解释得通了。七、常见坑可变对象改了参与 equals/hashCode 的字段就从 Set 里消失了对象入 Set 后改字段它的哈希桶位置变了再contains/remove找不到成了游离垃圾。规则放进 HashSet/HashMap 的 key 必须是不可变对象或至少别改参与哈希的字段。遍历时删除用迭代器for (String x : set) set.remove(x)抛ConcurrentModificationException要用it.remove()。HashSet 允许一个 null只有一个因为 key 不可重复TreeSet 不允许 null。八、面试追问QHashSet 怎么保证不重复元素作 HashMap 的 keyvalue 用固定占位对象add 时map.put返回非 null 说明 key 已存在返回 false 去重。Q为什么 HashSet 比 List 去重快List 去重要contains遍历 O(n)HashSet 靠哈希 O(1)。QHashSet 和 TreeSet 的 null 策略为何相反HashSet 底层 HashMap 允许一个 null keyTreeSet 底层 TreeMap 要靠比较器排 keynull 无法参与比较故不允许。总结Set 家族没有自己的存储全是借 MapHashSet 包 HashMap、LinkedHashSet 包 LinkedHashMap、TreeSet 包 TreeMap元素作 key、统一用占位 value。去重本质是 HashMap 的 key 不可重复前提是正确实现 equals/hashCode 且 key 不可变。按只去重 / 去重保序 / 去重排序三档选 HashSet / LinkedHashSet / TreeSet。