Java集合体系 —— Set篇
在Java集合框架中,Set接口是Collection接口的重要子接口,它定义了一组不包含重复元素、无索引的集合操作规范。与List接口的“有序、可重复、支持索引”特性形成鲜明对比,Set的核心价值在于“去重”,并通过不同实现类适配“无序存储”“保证插入顺序”“支持排序”等多样化需求。本文将从Set接口概述入手,深入解析HashSet、LinkedHashSet、TreeSet三大实现类的底层结构、核心源码、性能特点及适用场景,为实际开发提供选择依据。
一、Set接口概述
Set接口继承自Collection接口,其设计初衷是维护一组“唯一”的元素,所有实现类需遵守以下核心规范:
1. 核心特性
- 不可重复性:集合中不会存在两个通过
equals()
方法判断为true
的元素(具体去重逻辑由实现类决定,如哈希表或比较器)。 - 无索引:不支持通过
int
类型索引访问元素(如get(int index)
),遍历需依赖迭代器(Iterator)或增强for循环。 - 无序性(默认):除LinkedHashSet(保证插入顺序)和TreeSet(保证排序顺序)外,多数实现类(如HashSet)不保证元素的存储/遍历顺序与插入顺序一致。
2. 主要方法
Set接口的方法继承自Collection接口,核心方法围绕“去重”特性设计,关键方法如下:
方法签名 | 功能描述 | 核心特性 |
---|---|---|
boolean add(E e) | 向集合添加元素 | 元素不存在则添加并返回true ,存在则返回false (去重核心) |
boolean remove(Object o) | 移除指定元素 | 元素存在则移除并返回true ,否则返回false |
boolean contains(Object o) | 判断是否包含指定元素 | 依赖实现类的去重逻辑(如哈希表查找或红黑树查找) |
Iterator<E> iterator() | 返回元素迭代器 | 迭代顺序由实现类决定(无序/插入顺序/排序顺序) |
int size() | 返回元素个数 | - |
void clear() | 清空集合 | - |
boolean addAll(Collection<? extends E> c) | 添加指定集合的所有元素 | 仅添加当前集合中不存在的元素 |
Set接口本身不提供额外方法,所有差异化能力(如排序、顺序保证)均由具体实现类扩展。
二、HashSet详解与源码分析
HashSet是Set接口最常用的实现类,其底层通过HashMap的key存储元素(value固定为一个空对象),利用哈希表(数组+链表+红黑树,JDK 1.8+)实现高效的增删查操作,核心优势是“去重+高性能”。
1. 底层数据结构
HashSet的本质是“包装HashMap”,内部维护一个transient HashMap<E, Object>
对象,所有操作均委托给HashMap完成:
- 哈希表的“数组”用于存储元素的哈希值对应的索引位置;
- 当多个元素哈希值冲突时,通过“链表”存储;若链表长度超过8,则转为“红黑树”(优化查询性能,从O(n)降至O(log n));
- 用常量
PRESENT = new Object()
填充HashMap的value,避免额外内存开销。
2. 核心源码分析
(1)构造方法:初始化HashMap
HashSet的所有构造方法均围绕初始化HashMap展开,无额外逻辑:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable {private transient HashMap<E, Object> map;private static final Object PRESENT = new Object(); // 固定value// 空参构造:初始化默认HashMappublic HashSet() {map = new HashMap<>();}// 传入集合:初始化HashMap并添加集合元素public HashSet(Collection<? extends E> c) {// 计算初始容量:确保HashMap容量足够,避免频繁扩容map = new HashMap<>(Math.max((int) (c.size() / 0.75f) + 1, 16));addAll(c); // 调用AbstractCollection的addAll,本质是循环调用add(E e)}// 指定初始容量和负载因子:适配元素数量已知的场景public HashSet(int initialCapacity, float loadFactor) {map = new HashMap<>(initialCapacity, loadFactor);}
}
(2)添加元素:add(E e)
——去重的核心实现
HashSet的去重逻辑完全依赖HashMap的put(K key, V value)
方法:
public boolean add(E e) {// HashMap的put方法:key不存在则插入,返回null;key存在则覆盖,返回旧value// 因此add返回true表示元素新增,false表示元素已存在(去重)return map.put(e, PRESENT) == null;
}
- 关键逻辑:HashMap判断key是否存在时,先比较哈希值(
hashCode()
),若哈希值不同则为新元素;若哈希值相同,再通过**equals()
** 方法验证,若返回true
则视为重复元素,拒绝插入。 - 注意:若存储的元素未重写
hashCode()
和equals()
,会默认使用Object类的方法(仅判断对象地址),导致去重失效。
(3)查询元素:contains(Object o)
直接委托给HashMap的containsKey(Object key)
,利用哈希表快速查找特性:
public boolean contains(Object o) {return map.containsKey(o); // 哈希表查找时间复杂度O(1)
}
(4)删除元素:remove(Object o)
通过HashMap的remove(Object key)
移除元素,返回值表示是否移除成功:
public boolean remove(Object o) {// HashMap的remove返回被移除的value,若为PRESENT则表示元素存在并移除return map.remove(o) == PRESENT;
}
3. 性能特点
- 随机访问:无索引,不支持直接访问,但
contains()
和add()
操作时间复杂度为O(1)(哈希表无冲突时); - 添加/删除:尾部添加(无冲突)为O(1),若哈希冲突则需遍历链表/红黑树,最坏情况O(log n);
- 内存占用:哈希表存在“负载因子”(默认0.75),预留部分空桶以平衡性能和内存,存在少量空间浪费;
- 线程安全:非线程安全,多线程并发修改会抛出
ConcurrentModificationException
,需通过Collections.synchronizedSet(new HashSet<>())
或CopyOnWriteArraySet
实现同步。
4. 适用场景
- 仅需去重、不关心元素顺序的场景(如存储用户ID、过滤重复日志);
- 频繁执行添加、删除、查询操作,对性能要求高的场景。
三、LinkedHashSet详解与源码分析
LinkedHashSet是HashSet的子类,其底层在HashSet的哈希表基础上,额外维护了一个双向链表,用于记录元素的插入顺序,核心优势是“去重+保证插入顺序”。
1. 底层数据结构
LinkedHashSet的底层结构为“哈希表 + 双向链表”:
- 哈希表:继承自HashSet,保证增删查的高效性(同HashSet);
- 双向链表:每个元素节点额外存储
prev
(前驱)和next
(后继)引用,按插入顺序串联所有元素,确保遍历顺序与插入顺序一致; - 内部维护
transient LinkedHashMap.Entry<K, V>
类型的头节点(head
)和尾节点(tail
),记录链表的首尾位置。
2. 核心源码分析
(1)构造方法:初始化LinkedHashMap
LinkedHashSet本身无独立的核心方法,所有构造方法均调用HashSet的“隐藏构造方法”,初始化LinkedHashMap
而非普通HashMap:
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, Serializable {// 空参构造:调用HashSet的特殊构造方法,初始化LinkedHashMappublic LinkedHashSet() {super(16, 0.75f, true); // 第三个参数dummy为true,触发HashSet初始化LinkedHashMap}// 传入集合:指定初始容量,初始化LinkedHashMappublic LinkedHashSet(Collection<? extends E> c) {super(Math.max((int) (c.size() / 0.75f) + 1, 16), 0.75f, true);addAll(c);}
}// HashSet中的隐藏构造方法(仅LinkedHashSet调用)
HashSet(int initialCapacity, float loadFactor, boolean dummy) {map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
(2)关键特性:插入顺序的保证
LinkedHashSet的“插入顺序”由LinkedHashMap的LinkedHashMap.Entry
节点维护,每个新元素会被添加到双向链表的尾部:
// LinkedHashMap的add方法(间接调用)
void linkNodeLast(LinkedHashMap.Entry<K, V> p) {LinkedHashMap.Entry<K, V> last = tail;tail = p;if (last == null) {head = p; // 首次添加,头节点和尾节点均为当前元素} else {p.before = last; // 当前元素的前驱指向原尾节点last.after = p; // 原尾节点的后继指向当前元素}
}
遍历LinkedHashSet时,迭代器会按双向链表的head -> ... -> tail
顺序访问,因此遍历顺序与插入顺序完全一致。
(3)核心方法:继承自HashSet
LinkedHashSet未重写add()
、contains()
、remove()
等方法,完全复用HashSet的逻辑,但由于底层是LinkedHashMap,因此:
- 去重逻辑与HashSet一致(依赖
hashCode()
和equals()
); - 遍历性能优于HashSet(无需遍历哈希表的空桶,直接按链表顺序访问)。
3. 性能特点
- 随机访问:同HashSet,无索引,
contains()
和add()
为O(1),但需额外维护双向链表,性能略低于HashSet; - 遍历性能:时间复杂度O(n),且比HashSet更快(无空桶遍历开销);
- 内存占用:每个元素需额外存储
prev
和next
引用,内存开销高于HashSet; - 线程安全:同HashSet,非线程安全,需额外同步。
4. 适用场景
- 需要去重且必须保留元素插入顺序的场景(如记录用户操作日志、维护有序的唯一数据列表);
- 频繁遍历、较少随机访问的场景。
四、TreeSet详解与源码分析
TreeSet是唯一支持“排序”的Set实现类,其底层通过TreeMap的key存储元素,利用红黑树(自平衡二叉搜索树)实现元素的有序存储,核心优势是“去重+排序”。
1. 底层数据结构
TreeSet的本质是“包装TreeMap”,内部维护transient NavigableMap<E, Object>
对象(TreeMap实现了NavigableMap接口):
- 红黑树:一种自平衡的二叉搜索树,保证任意节点的左子树所有元素小于该节点,右子树所有元素大于该节点,从而实现“中序遍历即有序”;
- 排序方式分两种:自然排序(元素实现
Comparable
接口)和定制排序(构造时传入Comparator
); - 用常量
PRESENT = new Object()
填充TreeMap的value,与HashSet逻辑一致。
2. 核心源码分析
(1)构造方法:初始化TreeMap与排序规则
TreeSet的构造方法核心是指定排序规则(自然排序或定制排序):
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable {private transient NavigableMap<E, Object> m;private static final Object PRESENT = new Object();// 空参构造:默认自然排序,元素需实现Comparable接口public TreeSet() {this(new TreeMap<>());}// 传入Comparator:定制排序,按比较器规则排序public TreeSet(Comparator<? super E> comparator) {this(new TreeMap<>(comparator));}// 私有构造:接收NavigableMap(仅内部调用)TreeSet(NavigableMap<E, Object> m) {this.m = m;}
}
(2)添加元素:add(E e)
——排序与去重的核心
TreeSet的去重逻辑依赖红黑树的排序规则,而非equals()
方法:
public boolean add(E e) {// TreeMap的put方法:按排序规则插入key,若key已存在(compare返回0)则覆盖,返回旧valuereturn m.put(e, PRESENT) == null;
}
- 排序逻辑:TreeMap的
put
方法会调用compareTo()
(自然排序)或compare()
(定制排序)判断元素位置:- 若
compare
返回负数,元素插入左子树; - 若返回正数,插入右子树;
- 若返回0,视为重复元素,拒绝插入(即使
equals()
返回false
)。
- 若
- 注意:若使用自然排序,元素必须实现
Comparable
接口,否则抛出ClassCastException
;且不允许插入null
(无法排序)。
(3)排序相关方法:NavigableSet接口扩展
TreeSet实现了NavigableSet接口,提供丰富的排序相关方法(依赖TreeMap的导航能力):
// 返回集合中最小的元素
public E first() {return m.firstKey();
}// 返回集合中最大的元素
public E last() {return m.lastKey();
}// 返回小于e的最大元素
public E lower(E e) {return m.lowerKey(e);
}// 返回大于等于e的最小元素
public E ceiling(E e) {return m.ceilingKey(e);
}// 返回从start(含)到end(不含)的子集合
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) {return new TreeSet<>(m.subMap(fromElement, fromInclusive, toElement, toInclusive));
}
3. 性能特点
- 排序与查询:增删查操作时间复杂度均为O(log n)(红黑树的平衡与查找开销);
- 随机访问:无索引,通过
get(int index)
需遍历红黑树,时间复杂度O(n),性能较差; - 内存占用:红黑树节点需存储左子树、右子树、父节点引用及颜色标记,内存开销高于HashSet;
- 线程安全:非线程安全,多线程场景推荐使用
ConcurrentSkipListSet
(线程安全的有序Set,性能优于同步包装的TreeSet)。
4. 适用场景
- 需要对元素进行有序存储(升序/降序)且去重的场景(如按分数排序的学生名单、按时间排序的任务队列);
- 需要使用排序相关方法(如获取最大/最小元素、范围查询)的场景。
五、三种Set实现类的对比与适用场景
为清晰区分三者的差异,从底层结构、核心特性、性能等维度进行对比:
对比维度 | HashSet | LinkedHashSet | TreeSet |
---|---|---|---|
底层结构 | 哈希表(数组+链表+红黑树) | 哈希表 + 双向链表 | 红黑树(自平衡二叉搜索树) |
元素顺序 | 无序(随机) | 有序(插入顺序) | 有序(自然/定制排序) |
去重依据 | hashCode() + equals() | hashCode() + equals() | compareTo() /compare() |
是否允许null | 是(仅1个) | 是(仅1个) | 否(会抛NPE) |
时间复杂度(增删查) | O(1)(无冲突) | O(1)(略慢,需维护链表) | O(log n)(红黑树平衡) |
遍历性能 | 较慢(需遍历空桶) | 较快(按链表顺序) | 中等(红黑树中序遍历) |
线程安全 | 否 | 否 | 否 |
核心优势 | 高性能去重 | 去重+插入顺序保证 | 去重+排序 |
适用场景选择建议
-
HashSet:
- 优先选择场景:仅需去重、不关心顺序,且频繁执行增删查操作(如存储唯一标识、过滤重复数据);
- 避坑点:存储自定义对象时必须重写
hashCode()
和equals()
。
-
LinkedHashSet:
- 优先选择场景:需要去重且必须保留插入顺序,同时需要频繁遍历(如日志记录、历史操作跟踪);
- 避坑点:内存开销高于HashSet,不适合元素数量极大的场景。
-
TreeSet:
- 优先选择场景:需要有序存储(排序)且去重,或需要使用排序相关方法(如范围查询、获取极值);
- 避坑点:元素需实现
Comparable
(自然排序)或传入Comparator
(定制排序),不支持null。
六、总结
Set接口及三大实现类是Java集合框架中“去重”需求的核心解决方案,其设计各有侧重:
- HashSet:以“高性能”为核心,牺牲顺序,适合纯去重场景;
- LinkedHashSet:在HashSet基础上增加“插入顺序保证”,平衡性能与顺序需求;
- TreeSet:以“排序”为核心,牺牲部分性能,适合有序去重场景。
实际开发中,需根据“是否需要顺序”“需要何种顺序”“操作频率”三个核心维度选择实现类,同时注意线程安全问题(非线程安全实现类需额外同步),才能最大化集合的性能与易用性。