HashSet/LinkedHashSet/TreeSet 原理解析
第一章:Set 整体架构(前置基础,必记)
1.1 Set 接口核心特点
核心解析:Set接口继承自Collection接口,是Java集合体系中用于存储"不可重复元素"的核心接口,其核心特点的设计的逻辑,完全为"去重"和"高效访问"服务,与List接口(有序、可重复、有索引)形成鲜明对比。
核心特点(自用重点,必记):
-
不可重复:这是Set最核心的特性,无论哪种实现类,都不允许存储重复元素,去重逻辑依托底层Map的Key特性;
-
无索引:不提供通过下标访问元素的方法(如get(int index)),无法直接定位元素,访问方式依赖迭代或contains()判断;
-
有序性差异:并非所有Set都无序,HashSet无序、LinkedHashSet保持插入顺序、TreeSet保持自然排序(或自定义排序);
-
允许null:除TreeSet外,HashSet、LinkedHashSet均允许存储1个null元素(TreeSet因排序需求,无法存储null)。
面试提醒:常考"Set接口与List接口的区别",核心回答围绕"不可重复vs可重复、无索引vs有索引、有序性差异"展开,无需深入其他细节。
1.2 Set 三大实现类定位(面试高频)
核心解析:Set接口的三大常用实现类,底层均依赖对应的Map实现,本质是"复用Map的Key去重特性",仅封装了Set的对外接口,未重新实现核心逻辑,这是Set原理的核心,记牢这一点可快速掌握三者的底层逻辑。
三大实现类核心定位(联动记忆,必记):
-
HashSet:最常用的Set实现类,底层基于HashMap,追求高效访问,无有序性要求;
-
LinkedHashSet:继承自HashSet,底层基于LinkedHashMap,在高效访问的基础上,额外保证"插入顺序";
-
TreeSet:底层基于TreeMap,核心特性是"自动排序",牺牲部分效率,换取有序性。
自用重点+联动记忆:① 记牢"Set底层都是Map",所有Set的核心逻辑(去重、存储)均复用对应Map的逻辑,无需单独记忆;② 联动前文:HashMap对应HashSet、LinkedHashMap对应LinkedHashSet、TreeMap对应TreeSet,一一对应,降低记忆成本。
1.3 Set 底层核心共性(必记真理)
核心解析:所有Set实现类的底层,均采用"Map的Key存储元素",Value统一存储一个静态空对象(PRESENT),该对象无实际意义,仅用于填充Map的Value位置,因为Map的Key天然不可重复,恰好契合Set"不可重复"的核心需求。
简化源码(核心逻辑,无需深入完整源码):
java
// 所有Set底层共用的静态空对象,用于填充Map的Value
private static final Object PRESENT = new Object();
// 底层Map对象,所有Set的核心操作都委托给该Map
private transient HashMap<E, Object> map; // HashSet示例,其他Set类似
自用重点:记牢"Set = Map的Key + 固定空Value",后续解析三者原理时,均围绕这一核心展开,无需单独理解Set的去重、存储逻辑,只需关联对应Map的特性即可。
第二章:HashSet 原理解析(面试重点,最常用)
2.1 底层数据结构(必记)
核心解析:HashSet的底层完全依赖HashMap实现,无额外新增逻辑,其存储结构、高效访问的原理,与HashMap完全一致,可直接联动前文HashMap的底层结构记忆,无需单独推导。
核心结构(联动记忆):
-
底层对象:
private transient HashMap<E, Object> map;,所有HashSet的操作(add、remove、contains),均委托给该map对象执行; -
存储逻辑:HashSet中存储的元素,全部作为map的Key,每个Key对应的Value,都是同一个静态空对象(PRESENT);
-
底层结构:与HashMap一致,采用"数组(哈希表)+ 链表/红黑树"的结构,当链表长度≥8且数组长度≥64时,链表转为红黑树,优化查询效率。
自用重点:HashSet的底层 = HashMap,两者的底层结构、哈希冲突解决方式、高效访问原理完全一致,记牢这一点,可减少重复记忆。
2.2 核心构造器源码解析
核心解析:HashSet的构造器,本质是创建对应的HashMap对象,构造器的参数的逻辑,与HashMap的构造器完全一致,仅封装了一层Set的接口,简化源码如下(聚焦核心逻辑,省略冗余代码)。
java
// 1. 空参构造器:创建一个空的HashMap,与HashMap空参构造器逻辑一致
public HashSet() {
map = new HashMap<>();
}
// 2. 指定初始容量构造器:创建指定初始容量的HashMap,避免频繁扩容
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 3. 集合参数构造器:将传入集合的元素,作为map的Key,批量添加
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c); // 本质是调用map.putAll()
}
自用重点+面试提醒:① HashSet的构造器逻辑与HashMap完全一致,初始容量、负载因子(默认0.75)也与HashMap一致;② 面试常考"HashSet的初始容量是多少",核心回答:空参构造器初始容量为16(依托HashMap),与ArrayList初始容量(10)区分开。
2.3 核心方法源码解析(聚焦面试高频)
核心解析:HashSet的所有核心方法,均是对HashMap对应方法的简单封装,无额外逻辑,重点掌握add、remove、contains三个方法,这三个是面试高频考查点,联动HashMap的对应方法记忆即可。
2.3.1 add() 方法(去重核心,面试必问)
核心逻辑:添加元素时,本质是调用HashMap的put()方法,将元素作为Key,PRESENT作为Value,利用HashMap Key不可重复的特性,实现Set的去重。
java
public boolean add(E e) {
// 调用HashMap的put方法,Key为添加的元素,Value为固定空对象
return map.put(e, PRESENT) == null;
}
关键细节解析:
-
HashMap的put()方法,若Key不存在,返回null,此时add()方法返回true,表示添加成功;
-
若Key已存在(重复元素),put()方法返回原Value(PRESENT),此时add()方法返回false,表示添加失败,实现去重;
-
去重的判断标准,与HashMap Key的判断标准完全一致:先比较hashCode(),再比较equals(),两者都相同,才视为重复元素。
2.3.2 contains() 方法(高频)
核心逻辑:判断元素是否存在,本质是调用HashMap的containsKey()方法,判断Key是否存在,因为Set的元素就是HashMap的Key。
java
public boolean contains(Object o) {
return map.containsKey(o);
}
2.3.3 remove() 方法
核心逻辑:删除元素,本质是调用HashMap的remove()方法,删除对应的Key,返回值表示是否删除成功。
java
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
自用重点+联动记忆:① 所有HashSet的核心方法,均委托给HashMap执行,逻辑完全一致,无需单独记忆;② 联动前文HashMap的put、containsKey、remove方法,理解其底层逻辑,即可掌握HashSet的对应方法。
2.4 核心特性(面试必记)
-
有序性:无序,不保证元素的插入顺序,也不保证自然排序,元素的存储顺序由哈希表的散列位置决定(与HashMap一致);
-
去重:依托HashMap Key的不可重复特性,判断标准为hashCode() + equals();
-
线程安全:非线程安全,与HashMap一致,并发场景下可能出现数据异常,无同步锁;
-
null支持:允许存储1个null元素(因为HashMap允许Key为null);
-
效率:增删改查效率均为O(1),依托HashMap的哈希表结构,与HashMap效率一致。
2.5 自用重点 + 面试提醒
① 必记核心:HashSet底层 = HashMap,去重原理 = HashMap Key不可重复,判断标准 = hashCode() + equals();
② 面试高频提问1:"HashSet如何保证元素不重复?",核心回答:底层依赖HashMap,添加元素时作为Key,利用HashMap Key不可重复的特性,判断标准是hashCode()和equals()都相同;
③ 面试高频提问2:"HashSet为什么无序?",核心回答:底层是哈希表,元素存储位置由哈希值决定,与插入顺序无关;
④ 易错点:HashSet的无序≠随机,是哈希表的散列顺序,并非完全随机;
⑤ 补充细节:若要保证HashSet的去重逻辑正确,存储的元素必须重写hashCode()和equals()方法,否则会出现去重失效(与HashMap Key的要求一致)。
第三章:LinkedHashSet 原理解析(面试重点,插入有序)
3.1 底层数据结构(必记)
核心解析:LinkedHashSet继承自HashSet,底层并非重新实现,而是依赖LinkedHashMap实现,其核心结构是"哈希表 + 双向链表",既保留了HashSet的高效访问特性,又通过双向链表保证了元素的插入顺序,联动前文LinkedHashMap的结构记忆即可。
核心结构(联动记忆):
-
继承关系:
public class LinkedHashSet<E> extends HashSet<E>,继承HashSet的所有方法,仅重写了构造器; -
底层对象:底层实际是LinkedHashMap,HashSet中的map属性,在LinkedHashSet中被赋值为LinkedHashMap对象;
-
结构特性:哈希表(保证O(1)效率)+ 双向链表(维护插入顺序),双向链表记录元素的插入顺序,迭代时按插入顺序输出。
自用重点:LinkedHashSet = HashSet + LinkedHashMap,底层结构、效率与HashSet一致,仅多了双向链表维护插入顺序,无需单独记忆新的结构逻辑。
3.2 核心构造器源码解析
核心解析:LinkedHashSet没有新增构造器,仅重写了HashSet的构造器,将底层的map对象,从HashMap改为LinkedHashMap,简化源码如下(聚焦核心逻辑)。
java
// 空参构造器:创建LinkedHashMap,初始容量16,负载因子0.75
public LinkedHashSet() {
super(16, .75f, true); // 调用HashSet的带参构造器,第三个参数指定为true,标识底层是LinkedHashMap
}
// 指定初始容量构造器
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
// 集合参数构造器
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max((int) (c.size()/.75f) + 1, 16), .75f, true);
addAll(c);
}
关键细节:HashSet中有一个隐藏的带参构造器(访问权限为包私有),第三个参数"accessOrder"用于指定底层是否为LinkedHashMap,LinkedHashSet调用该构造器时,传入accessOrder=true,确保底层创建的是LinkedHashMap。
自用重点:LinkedHashSet的构造器,本质是创建LinkedHashMap,初始容量、负载因子与HashSet、HashMap一致,无需单独记忆。
3.3 核心特性(与HashSet对比记忆,面试必记)
-
有序性:插入有序,通过双向链表维护元素的插入顺序,迭代时按插入顺序输出,这是与HashSet的核心区别;
-
去重:与HashSet完全一致,依托LinkedHashMap Key的不可重复特性,判断标准为hashCode() + equals();
-
线程安全:非线程安全,与HashSet、LinkedHashMap一致,无同步锁;
-
null支持:允许存储1个null元素(与LinkedHashMap一致);
-
效率:增删改查效率仍为O(1),但略低于HashSet,因为多了双向链表的维护(插入、删除时需修改链表指针)。
3.4 有序性原理(面试高频)
核心解析:LinkedHashSet的有序性,完全依托LinkedHashMap的双向链表实现,与LinkedHashMap的有序性原理一致,无需单独理解。
关键逻辑:
-
每个元素在存储时,除了存入哈希表,还会加入双向链表中;
-
双向链表按元素的插入顺序,记录每个元素的前驱和后继节点;
-
迭代时,不遍历哈希表,而是遍历双向链表,因此能保证按插入顺序输出。
自用重点+联动记忆:联动前文LinkedHashMap的有序性原理,LinkedHashSet的有序性与之一致,都是"双向链表维护插入顺序",无需单独推导。
3.5 自用重点 + 面试提醒
① 必记核心:LinkedHashSet底层 = LinkedHashMap,核心优势是"去重 + 插入有序",效率略低于HashSet;
② 面试高频提问:"HashSet和LinkedHashSet的区别是什么?",核心回答:两者底层分别是HashMap和LinkedHashMap,核心区别是LinkedHashSet保证插入有序,HashSet无序,效率上HashSet略高;
③ 易错点:LinkedHashSet的有序是"插入有序",不是自然排序,与TreeSet的有序性区分开;
④ 适用场景:需要去重,且需要保证元素插入顺序的场景(如日志存储、历史记录)。
第四章:TreeSet 原理解析(面试重点,自动排序)
4.1 底层数据结构(必记)
核心解析:TreeSet的底层依赖TreeMap实现,与HashSet、LinkedHashSet的底层逻辑一致,均是"复用Map的Key特性",但TreeMap的底层结构是红黑树,因此TreeSet的核心特性是"自动排序",联动前文TreeMap的结构记忆即可。
核心结构(联动记忆):
-
底层对象:
private transient NavigableMap<E, Object> m;,实际是TreeMap对象,TreeSet的所有操作均委托给该对象; -
存储逻辑:TreeSet中的元素作为TreeMap的Key,Value同样是固定空对象(PRESENT);
-
底层结构:与TreeMap一致,采用红黑树结构,红黑树的特性是"平衡二叉树",能保证元素的有序性,增删改查效率为O(log n)。
自用重点:TreeSet底层 = TreeMap,排序原理、底层结构与TreeMap完全一致,无需单独记忆新的逻辑,重点掌握排序相关的细节即可。
4.2 核心构造器源码解析
核心解析:TreeSet的构造器,本质是创建TreeMap对象,支持自然排序和自定义排序两种方式,与TreeMap的构造器逻辑完全一致,简化源码如下。
java
// 1. 空参构造器:自然排序,元素必须实现Comparable接口
public TreeSet() {
this(new TreeMap<>());
}
// 2. 自定义排序:传入Comparator比较器,按比较器逻辑排序
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
// 3. 集合参数构造器:自然排序,将集合元素作为TreeMap的Key
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
关键细节解析:
-
自然排序:空参构造器创建的TreeSet,默认采用自然排序,要求存储的元素必须实现Comparable接口,重写compareTo()方法,否则会抛出ClassCastException;
-
自定义排序:传入Comparator比较器,无需元素实现Comparable接口,按比较器的compare()方法逻辑排序,灵活性更高;
-
底层关联:所有构造器最终都是创建TreeMap对象,TreeSet的排序逻辑,完全依赖TreeMap的排序逻辑。
4.3 排序原理(面试必问)
核心解析:TreeSet的自动排序,本质是TreeMap的红黑树排序逻辑,红黑树会根据Key的比较结果,自动调整树的结构,保证所有Key按指定顺序排列,迭代时按排序后的顺序输出。
两种排序方式(必记):
4.3.1 自然排序(默认)
-
要求:元素必须实现Comparable接口,重写compareTo(T o)方法;
-
排序逻辑:按compareTo()方法的返回值排序,返回负数表示当前元素小于参数元素,返回正数表示大于,返回0表示相等(视为重复元素);
-
示例:Integer、String等常用类,已实现Comparable接口,默认按升序排序。
4.3.2 自定义排序
-
要求:创建TreeSet时,传入Comparator比较器,重写compare(T o1, T o2)方法;
-
排序逻辑:按compare()方法的返回值排序,逻辑与compareTo()一致;
-
优势:无需修改元素类的代码,可灵活定义排序规则(如降序、按自定义字段排序)。
自用重点+面试提醒:① 排序的核心是"比较逻辑",无论是自然排序还是自定义排序,都需要明确元素的比较规则;② 面试常考"TreeSet的排序方式有哪些",核心回答:自然排序(Comparable)和自定义排序(Comparator);③ 去重与排序的关联:TreeSet的去重,是通过比较逻辑判断的(compareTo()或compare()返回0,视为重复),无需重写hashCode()和equals()(但建议重写,保证逻辑一致性)。
4.4 核心特性(面试必记)
-
有序性:自动排序,默认自然升序,可通过Comparator实现自定义排序,与LinkedHashSet的"插入有序"区分开;
-
去重:依托TreeMap的Key比较逻辑,compareTo()或compare()返回0,视为重复元素,无需重写hashCode()和equals();
-
线程安全:非线程安全,与TreeMap一致,无同步锁;
-
null支持:不允许存储null,因为null无法参与比较(会抛出NullPointerException);
-
效率:增删改查效率为O(log n),低于HashSet和LinkedHashSet,因为红黑树的插入、删除需要维护树的平衡。
4.5 自用重点 + 面试提醒
① 必记核心:TreeSet底层 = TreeMap,核心优势是"去重 + 自动排序",效率O(log n),不允许null;
② 面试高频提问1:"TreeSet为什么不能存null?",核心回答:TreeSet需要对元素进行排序,null无法参与比较,会抛出空指针异常;
③ 面试高频提问2:"TreeSet的去重原理是什么?",核心回答:依托排序逻辑,通过compareTo()或compare()方法判断,返回0视为重复元素;
④ 易错点:TreeSet的有序是"自动排序",不是插入有序,与LinkedHashSet区分开;元素未实现Comparable接口且未传入比较器,会抛出ClassCastException;
⑤ 适用场景:需要去重,且需要元素自动排序的场景(如排行榜、有序数据集)
第五章:三大Set 核心对比(面试重中之重,必背)
5.1 核心维度对比(自用记忆表格,清晰直观)
| 对比维度 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层实现 | HashMap | LinkedHashMap | TreeMap |
| 底层结构 | 数组+链表/红黑树 | 哈希表+双向链表 | 红黑树 |
| 有序性 | 无序(散列顺序) | 插入有序 | 自动排序(自然/自定义) |
| 去重原理 | hashCode() + equals() | hashCode() + equals() | compareTo()/compare() |
| null支持 | 允许1个 | 允许1个 | 不允许 |
| 时间复杂度 | O(1) | O(1)(略低) | O(log n) |
| 线程安全 | 否 | 否 | 否 |
| 适用场景 | 仅需去重,追求高效 | 去重+保持插入顺序 | 去重+自动排序 |
5.2 面试高频提问及核心回答(直接背诵,适配面试)
提问1:HashSet、LinkedHashSet、TreeSet 的核心区别是什么?
回答:核心区别在于底层实现和有序性:① HashSet底层HashMap,无序,效率O(1);② LinkedHashSet底层LinkedHashMap,插入有序,效率略低于HashSet;③ TreeSet底层TreeMap,自动排序,效率O(log n),不允许null。
提问2:HashSet 如何保证元素不重复?
回答:底层依赖HashMap,添加元素时作为HashMap的Key,利用HashMap Key不可重复的特性实现去重;判断重复的标准是:先比较hashCode(),再比较equals(),两者都相同视为重复元素。
提问3:TreeSet 和 LinkedHashSet 的有序性有什么区别?
回答:TreeSet是"自动排序",默认自然升序,可通过Comparator自定义排序;LinkedHashSet是"插入有序",仅保证迭代顺序与插入顺序一致,不进行排序。
提问4:为什么 TreeSet 不能存 null,而 HashSet 可以?
回答:TreeSet需要对元素进行排序,null无法参与比较(会抛出空指针异常);HashSet底层是HashMap,HashMap允许Key为null,因此HashSet可以存1个null。
提问5:Set 是线程安全的吗?并发场景下如何处理?
回答:HashSet、LinkedHashSet、TreeSet 都不是线程安全的;并发场景下,可使用 Collections.synchronizedSet() 包装,或使用 CopyOnWriteArraySet(效率更高,适合读多写少场景)。
第六章:面试易错点汇总(自用避坑,必记)
6.1 概念误区纠正(高频易错)
-
误区1:HashSet 是随机无序的 → 纠正:HashSet是无序的,但不是随机,是哈希表的散列顺序,由元素的hashCode()决定;
-
误区2:LinkedHashSet 是排序的 → 纠正:LinkedHashSet是"插入有序",不是排序,仅保证迭代顺序与插入顺序一致,不进行升序/降序排序;
-
误区3:TreeSet 去重需要重写 hashCode() 和 equals() → 纠正:TreeSet的去重依托排序逻辑(compareTo()/compare()),无需重写这两个方法,但建议重写,保证逻辑一致性;
-
误区4:所有 Set 都不允许存 null → 纠正:仅 TreeSet 不允许存 null,HashSet、LinkedHashSet 均允许存1个null;
-
误区5:LinkedHashSet 效率比 HashSet 高 → 纠正:LinkedHashSet效率略低于HashSet,因为多了双向链表的维护成本;
-
误区6:Set 底层是独立实现的 → 纠正:所有 Set 的底层都是 Map,仅使用 Map 的 Key 存储元素,Value 是固定空对象。
6.2 版本差异(JDK 1.7 vs 1.8,聚焦面试高频)
-
HashSet:1.7与1.8核心逻辑无差异,仅HashMap底层结构优化(1.8链表转红黑树),间接影响HashSet的效率;
-
LinkedHashSet:1.7与1.8无核心差异,仅LinkedHashMap的源码细节优化,不影响对外特性;
-
TreeSet:1.7与1.8无核心差异,红黑树的平衡逻辑略有优化,不影响排序和去重特性,无需深入细节。
第七章:总结与自用复习建议
------ 核心知识点串联:Set三大实现类,均依托Map实现(HashSet→HashMap、LinkedHashSet→LinkedHashMap、TreeSet→TreeMap),核心差异在于有序性和底层结构,联动前文Map的知识点,可大幅降低记忆成本;
------ 复习重点:记牢"底层实现+有序性+去重原理+效率"四大核心,重点背诵第五章的对比表格和面试高频提问,无需背诵完整源码;
------ 面试技巧:回答Set相关问题时,先讲底层实现,再讲核心特性(有序性、去重、null支持),最后讲适用场景,逻辑更清晰,更易获得面试官认可;
------ 自用口诀(快速记忆):HashSet(快、无序),LinkedHashSet(快、插入有序),TreeSet(慢、自动排序、不存null),底层全是Map。
附录(快速查阅,自用便捷)
------ 核心属性汇总
-
公共属性:private static final Object PRESENT = new Object();(所有Set共用的空Value);
-
HashSet:private transient HashMap<E, Object> map;(底层HashMap对象);
-
LinkedHashSet:继承HashSet,底层map为LinkedHashMap对象;
-
TreeSet:private transient NavigableMap<E, Object> m;(底层TreeMap对象)。
------ 高频方法核心源码简化版(直接背诵,应对源码提问)
java
// HashSet add() 核心源码
public boolean add(E e) { return map.put(e, PRESENT) == null; }
// LinkedHashSet 构造器核心源码
public LinkedHashSet() { super(16, .75f, true); }
// TreeSet 排序核心(自然排序)
public TreeSet() { this(new TreeMap<>()); }