HashSet/LinkedHashSet/TreeSet 原理深度解析

HashSet/LinkedHashSet/TreeSet 原理深度解析(JDK1.8)

在Java集合框架中,Set接口用于存储"无序、无索引、不可重复"的元素,其核心实现类为HashSet、LinkedHashSet和TreeSet。三者虽均遵循Set接口规范,但底层依赖不同的数据结构,导致其在有序性、性能、去重逻辑上存在显著差异。本文基于JDK 1.8源码,从继承体系、底层结构、去重原理、核心方法等8大核心维度,对三者进行全方位解析,既适合个人复习,也能应对面试中的核心考点。

一、概述:Set集合的核心特性

Set接口继承自Collection接口,与List接口最大的区别的是:

  • 无索引:无法通过下标直接访问元素,遍历需通过迭代器、增强for循环;

  • 不可重复:集合中不会存在两个equals()返回true的元素;

  • 无序性(部分实现类除外):默认情况下,元素的插入顺序与遍历顺序不一致(TreeSet、LinkedHashSet除外)。

HashSet、LinkedHashSet、TreeSet作为Set接口的核心实现,其底层均依赖Java中的Map集合(本质是"Map的包装类"),通过将Set的元素作为Map的key,value统一存储一个固定的空对象,间接实现Set的所有功能。这是理解三者原理的核心前提。

二、继承体系与实现接口(定位差异)

三者的继承关系直接决定了其功能定位,先通过源码明确各自的继承与实现结构:

1. HashSet 继承结构

java 复制代码
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    // 核心:底层依赖HashMap存储元素
    private transient HashMap<E, Object> map;
    // 固定的空对象,作为Map的value
    private static final Object PRESENT = new Object();
    // 其他代码...
}

核心特点:继承AbstractSet(简化Set接口实现),实现Set、Cloneable、Serializable接口,无额外扩展功能,核心定位是"高效去重、无序存储"。

2. LinkedHashSet 继承结构

java 复制代码
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    // 构造方法直接调用父类HashSet的构造,指定底层为LinkedHashMap
    public LinkedHashSet() {
        super(16, .75f, true);
    }
    // 其他代码...
}

核心特点:继承自HashSet,因此完全具备HashSet的去重特性,同时底层依赖LinkedHashMap,额外实现了"保留插入顺序"的功能。

3. TreeSet 继承结构

java 复制代码
public class TreeSet<E>
    extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable {
    // 核心:底层依赖TreeMap存储元素
    private transient NavigableMap<E, Object> m;
    // 固定的空对象,作为Map的value
    private static final Object PRESENT = new Object();
    // 其他代码...
}

核心特点:不继承HashSet,直接继承AbstractSet,实现NavigableSet接口(NavigableSet继承自SortedSet),底层依赖TreeMap,核心定位是"排序去重、支持导航查询"(如获取首尾元素、范围查询)。

继承体系总结

  • LinkedHashSet → HashSet → AbstractSet → Set;

  • TreeSet → AbstractSet → Set;

  • 三者的核心共性:都通过"Map的key"实现去重,value均为固定空对象PRESENT。

三、底层数据结构(核心差异)

三者的性能、有序性差异,本质源于底层依赖的Map结构不同。前面已经提到,Set是Map的"包装类",因此底层结构完全复用对应Map的结构:

1. HashSet 底层:HashMap(哈希表)

HashSet的底层就是HashMap,源码中明确定义了map属性(如上面的HashSet源码所示)。HashMap的底层结构是"数组+链表+红黑树"(JDK1.8):

  • 数组(哈希桶):存储链表/红黑树的头节点,数组下标通过元素的hashCode()计算得出;

  • 链表:解决哈希冲突(不同元素hashCode相同),当链表长度超过8时,转为红黑树;

  • 红黑树:提升哈希冲突严重时的查询、插入、删除效率(从O(n)优化为O(log n))。

HashSet的元素,就是HashMap的key;HashSet的所有操作(add、remove等),本质都是调用HashMap对应的方法。

2. LinkedHashSet 底层:LinkedHashMap

LinkedHashSet继承自HashSet,但其构造方法会调用HashSet的一个重载构造,强制指定底层为LinkedHashMap:

java 复制代码
// HashSet的重载构造(仅LinkedHashSet调用)
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

LinkedHashMap的底层是"HashMap的哈希表 + 双向链表":

  • 哈希表:复用HashMap的结构,保证去重和高效查询;

  • 双向链表:额外维护一个双向链表,记录元素的插入顺序,遍历元素时按照链表顺序执行,因此能保留插入顺序。

3. TreeSet 底层:TreeMap(红黑树)

TreeSet的底层是TreeMap,TreeMap的底层结构是红黑树(一种自平衡的二叉搜索树):

  • 红黑树特性:左子树所有节点值 < 根节点值,右子树所有节点值 > 根节点值,通过旋转保持树的平衡;

  • 排序核心:TreeMap会根据key的大小自动排序,因此TreeSet的元素也会自动排序(自然排序或自定义排序);

  • 无哈希表:TreeMap不依赖hashCode(),而是通过比较器(Comparable/Comparator)确定元素的位置,因此TreeSet的去重逻辑也与哈希无关。

底层结构对比表

集合 底层依赖 核心结构 核心优势
HashSet HashMap 数组+链表+红黑树 去重高效,查询、插入、删除速度快
LinkedHashSet LinkedHashMap 哈希表+双向链表 去重高效,保留插入顺序
TreeSet TreeMap 红黑树 自动排序,支持导航查询

四、元素唯一性(去重原理,Set核心功能)

Set的核心价值是"去重",三者的去重逻辑完全依赖底层Map的key去重逻辑,分为两类:Hash系列(HashSet、LinkedHashSet)和Tree系列(TreeSet)。

1. HashSet & LinkedHashSet 去重原理(基于哈希)

两者底层都是HashMap,去重逻辑完全一致,核心依赖"hashCode() + equals()"两个方法,流程如下(以add方法为例):

  1. 当调用add(E e)方法时,底层调用HashMap的put(e, PRESENT)方法;

  2. 计算元素e的hashCode()值,根据hashCode()计算出哈希桶下标(确定元素在数组中的位置);

  3. 判断该下标对应的位置是否有元素:

    • 无元素:直接插入该元素(作为新节点存入哈希表);

    • 有元素(哈希冲突):调用equals()方法对比两个元素是否相等;

      • equals()返回true:视为重复元素,不插入;

      • equals()返回false:视为不同元素,存入链表/红黑树中。

关键注意:重写equals()方法时,必须重写hashCode()方法,否则会导致去重失效。例如:两个对象equals()返回true,但hashCode()不同,会被存入哈希表的不同位置,视为两个不同元素。

2. TreeSet 去重原理(基于比较器)

TreeSet底层是TreeMap,不依赖hashCode()和equals(),而是通过"比较器"判断元素是否重复,核心逻辑如下:

  1. TreeSet有两种排序方式,对应两种比较逻辑:

    • 自然排序:元素实现Comparable接口,重写compareTo(E o)方法;

    • 自定义排序:创建TreeSet时,传入Comparator接口实现类,重写compare(E o1, E o2)方法。

  2. 当调用add(E e)方法时,底层调用TreeMap的put(e, PRESENT)方法;

  3. 通过比较器判断元素e与已有元素的大小关系:

    • 若compareTo()/compare()返回0:视为重复元素,不插入;

    • 若返回正数/负数:视为不同元素,按照红黑树的排序规则插入对应位置。

注意:TreeSet中,若元素未实现Comparable接口,且创建时未传入Comparator,会抛出ClassCastException(类型转换异常)。

五、有序性对比(三者最大区别之一)

有序性是三者最直观的区别,结合底层结构,可清晰理解其有序性的本质:

1. HashSet:无序

底层是HashMap的哈希表,元素的存储位置由hashCode()计算决定,与插入顺序无关。遍历HashSet时,元素的顺序是"哈希桶顺序 + 链表/红黑树顺序",无法保证与插入顺序一致。

示例:插入1、3、2,遍历结果可能是1、2、3,也可能是3、1、2,完全取决于hashCode()计算的下标。

2. LinkedHashSet:保留插入顺序

底层是LinkedHashMap,在哈希表的基础上,额外维护了一个双向链表,链表的节点顺序与元素的插入顺序一致。遍历LinkedHashSet时,会按照双向链表的顺序遍历,因此能严格保留插入顺序。

示例:插入1、3、2,遍历结果一定是1、3、2,与插入顺序完全一致。

3. TreeSet:自然排序/自定义排序

底层是TreeMap的红黑树,元素会按照比较器的规则自动排序,与插入顺序无关。排序规则由Comparable/Comparator决定:

  • 自然排序:默认按照元素的"自然顺序"排序(如Integer从小到大、String按字典序);

  • 自定义排序:按照传入的Comparator规则排序(如Integer从大到小、自定义对象按某个属性排序)。

示例:插入3、1、2,自然排序下遍历结果是1、2、3;自定义排序(从大到小)下遍历结果是3、2、1。

六、null值支持情况

三者对null值的支持,同样由底层Map决定,差异明显:

1. HashSet:允许存储1个null值

底层HashMap允许key为null(仅1个),因此HashSet也允许存储1个null值。当插入第二个null时,会被视为重复元素,无法插入。

2. LinkedHashSet:允许存储1个null值

底层LinkedHashMap继承自HashMap,同样允许key为null(仅1个),因此LinkedHashSet也支持1个null值,且null值会被纳入双向链表,保留插入顺序。

3. TreeSet:不允许存储null值

底层TreeMap的key需要通过比较器排序,而null值无法调用compareTo()方法(会抛出NullPointerException),因此TreeSet不允许插入null值。

java 复制代码
// 错误示例:TreeSet插入null会报错
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(null); // 抛出NullPointerException

七、核心方法原理(add/remove/contains)

三者的核心方法(add、remove、contains)均是"委托"底层Map的对应方法实现,逻辑简单,源码层面高度复用:

1. add(E e) 方法

  • HashSet:return map.put(e, PRESENT) == null;(HashMap的put方法返回旧值,若为null,说明是新元素,插入成功);

  • LinkedHashSet:继承HashSet的add方法,底层调用LinkedHashMap的put方法;

  • TreeSet:return m.put(e, PRESENT) == null;(m是TreeMap对象,逻辑与HashSet一致)。

2. remove(Object o) 方法

  • HashSet:return map.remove(o) == PRESENT;(HashMap的remove方法返回旧值,若等于PRESENT,说明删除成功);

  • LinkedHashSet:继承HashSet的remove方法,底层调用LinkedHashMap的remove方法;

  • TreeSet:return m.remove(o) == PRESENT;(逻辑与HashSet一致)。

3. contains(Object o) 方法

  • HashSet:return map.containsKey(o);(判断HashMap中是否存在该key);

  • LinkedHashSet:继承HashSet的contains方法,底层调用LinkedHashMap的containsKey方法;

  • TreeSet:return m.containsKey(o);(判断TreeMap中是否存在该key)。

总结:Set的所有方法都没有自己的核心逻辑,完全依赖底层Map的实现,这也是"Set是Map包装类"的核心体现。

八、性能对比(时间复杂度)

性能差异源于底层数据结构,主要对比"查找、插入、删除"三个核心操作的时间复杂度,结合JDK1.8的优化(链表转红黑树),具体如下:

集合 查找(contains) 插入(add) 删除(remove) 备注
HashSet O(1) O(1) O(1) 哈希冲突严重时,退化为O(log n)(红黑树)
LinkedHashSet O(1) O(1) O(1) 比HashSet略慢,需维护双向链表的指针
TreeSet O(log n) O(log n) O(log n) 红黑树的平衡操作带来额外开销
性能总结:HashSet > LinkedHashSet > TreeSet(整体),其中HashSet是三者中性能最优的,TreeSet因排序开销,性能相对较低。

九、使用场景选择(落地实战)

根据三者的核心特性,结合实际开发场景,选择对应的Set实现类,避免盲目使用:

1. 优先使用 HashSet

核心场景:只需要"去重",不关心元素顺序,追求最高性能。

  • 示例:存储用户ID、商品ID(唯一标识,无需排序);

  • 优势:性能最优,内存开销适中,是开发中最常用的Set实现。

2. 优先使用 LinkedHashSet

核心场景:需要"去重 + 保留插入顺序",对性能要求较高。

  • 示例:存储用户操作日志(去重,且需要按操作顺序展示)、去重后的有序列表;

  • 优势:兼顾去重效率和有序性,比TreeSet性能高,比HashSet多了有序特性。

3. 优先使用 TreeSet

核心场景:需要"自动排序 + 去重",或需要导航查询(如获取最大/最小元素、范围查询)。

  • 示例:排行榜(按分数排序去重)、范围查询(获取100-200之间的元素);

  • 优势:自动排序,支持navigableSet接口的特有方法(如first()、last()、subSet())。

十、关键注意事项(避坑指南)

  1. HashSet/LinkedHashSet:重写元素的equals()和hashCode()方法,否则去重失效;

  2. TreeSet:元素必须实现Comparable接口,或创建时传入Comparator,否则抛出类型转换异常;

  3. 线程安全:三者均为非线程安全集合,多线程环境下,需使用Collections.synchronizedSet()包装,或使用CopyOnWriteArraySet;

  4. LinkedHashSet的内存开销:比HashSet多维护双向链表,内存开销略大;

  5. TreeSet的排序陷阱:若比较器逻辑修改,可能导致元素排序异常或去重失效。

十一、总结(面试高频考点)

HashSet、LinkedHashSet、TreeSet的核心差异,本质是底层Map的差异(HashMap → LinkedHashMap → TreeMap),总结如下:

  • 去重逻辑:Hash系列(hashCode() + equals()),Tree系列(比较器);

  • 有序性:HashSet无序,LinkedHashSet插入有序,TreeSet排序有序;

  • 性能:HashSet最优,LinkedHashSet次之,TreeSet最差;

  • 核心定位:HashSet(高效去重)、LinkedHashSet(去重+有序)、TreeSet(排序+去重)。

记住一句话:"查多用HashSet,有序用LinkedHashSet,排序用TreeSet",即可覆盖绝大多数开发场景,也能快速应对面试中的核心提问。

相关推荐
CQU_JIAKE2 小时前
3.23[Q]s
开发语言·windows·python
2401_831824962 小时前
高性能压缩库实现
开发语言·c++·算法
2401_874732532 小时前
C++中的策略模式进阶
开发语言·c++·算法
steins_甲乙2 小时前
C# 通过共享内存与 C++ 宿主协同捕获软件窗口
开发语言·c++·c#·内存共享
章鱼丸-2 小时前
DAY34 GPU 训练与类的 call 方法
开发语言·python
2501_945423542 小时前
C++跨平台开发实战
开发语言·c++·算法
英俊潇洒美少年2 小时前
函数组件(Hooks)的 **10 大优点**
开发语言·javascript·react.js
Oueii2 小时前
分布式系统监控工具
开发语言·c++·算法
小陈工2 小时前
2026年3月24日技术资讯洞察:边缘AI商业化,Java26正式发布与开源大模型成本革命
java·运维·开发语言·人工智能·python·容器·开源