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方法为例):
-
当调用add(E e)方法时,底层调用HashMap的put(e, PRESENT)方法;
-
计算元素e的hashCode()值,根据hashCode()计算出哈希桶下标(确定元素在数组中的位置);
-
判断该下标对应的位置是否有元素:
-
无元素:直接插入该元素(作为新节点存入哈希表);
-
有元素(哈希冲突):调用equals()方法对比两个元素是否相等;
-
equals()返回true:视为重复元素,不插入;
-
equals()返回false:视为不同元素,存入链表/红黑树中。
-
-
关键注意:重写equals()方法时,必须重写hashCode()方法,否则会导致去重失效。例如:两个对象equals()返回true,但hashCode()不同,会被存入哈希表的不同位置,视为两个不同元素。
2. TreeSet 去重原理(基于比较器)
TreeSet底层是TreeMap,不依赖hashCode()和equals(),而是通过"比较器"判断元素是否重复,核心逻辑如下:
-
TreeSet有两种排序方式,对应两种比较逻辑:
-
自然排序:元素实现Comparable接口,重写compareTo(E o)方法;
-
自定义排序:创建TreeSet时,传入Comparator接口实现类,重写compare(E o1, E o2)方法。
-
-
当调用add(E e)方法时,底层调用TreeMap的put(e, PRESENT)方法;
-
通过比较器判断元素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())。
十、关键注意事项(避坑指南)
-
HashSet/LinkedHashSet:重写元素的equals()和hashCode()方法,否则去重失效;
-
TreeSet:元素必须实现Comparable接口,或创建时传入Comparator,否则抛出类型转换异常;
-
线程安全:三者均为非线程安全集合,多线程环境下,需使用Collections.synchronizedSet()包装,或使用CopyOnWriteArraySet;
-
LinkedHashSet的内存开销:比HashSet多维护双向链表,内存开销略大;
-
TreeSet的排序陷阱:若比较器逻辑修改,可能导致元素排序异常或去重失效。
十一、总结(面试高频考点)
HashSet、LinkedHashSet、TreeSet的核心差异,本质是底层Map的差异(HashMap → LinkedHashMap → TreeMap),总结如下:
-
去重逻辑:Hash系列(hashCode() + equals()),Tree系列(比较器);
-
有序性:HashSet无序,LinkedHashSet插入有序,TreeSet排序有序;
-
性能:HashSet最优,LinkedHashSet次之,TreeSet最差;
-
核心定位:HashSet(高效去重)、LinkedHashSet(去重+有序)、TreeSet(排序+去重)。
记住一句话:"查多用HashSet,有序用LinkedHashSet,排序用TreeSet",即可覆盖绝大多数开发场景,也能快速应对面试中的核心提问。