第1章:HashSet深度剖析
1.1 HashSet基础原理
1.1.1 底层基于HashMap实现
核心数据结构
HashSet的底层实现非常简单,它基于HashMap实现:
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 核心:底层使用HashMap存储
private transient HashMap<E,Object> map;
// 虚拟值,所有元素都映射到这个值
private static final Object PRESENT = new Object();
}
设计特点:
- HashSet内部维护一个HashMap
- 所有元素作为HashMap的key存储
- value统一使用PRESENT常量(虚拟值)
为什么使用HashMap?
优势:
- 代码复用: 不需要重复实现哈希表逻辑
- 性能保证: HashMap已经优化得很好
- 维护简单: 代码量少,易于维护
结构示意:
HashSet
↓
HashMap
├─ Key: 实际元素
└─ Value: PRESENT(虚拟值)
1.1.2 构造函数
默认构造函数
public HashSet() {
map = new HashMap<>();
}
特点:
- 创建默认的HashMap(容量16,负载因子0.75)
- 适合大多数场景
指定初始容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
使用场景:
- 知道大概的元素数量
- 避免频繁扩容
指定初始容量和负载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
使用场景:
- 需要精确控制容量和负载因子
- 特殊性能要求
从Collection构造
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
特点:
- 根据集合大小计算初始容量
- 避免扩容
1.2 元素唯一性保证机制
1.2.1 hashCode() + equals()双重检查
唯一性判断流程
HashSet通过**hashCode() + equals()**双重检查来保证元素唯一性:
add(element)
↓
计算hashCode()
↓
定位HashMap位置
↓
位置是否为空?
├─ 是 → 直接添加
└─ 否 → 遍历链表/红黑树
├─ hashCode()相同?
│ ├─ 是 → equals()比较
│ │ ├─ true → 不添加(已存在)
│ │ └─ false → 添加(冲突)
│ └─ 否 → 继续查找
↓
完成
为什么需要双重检查?
hashCode()的作用:
- 快速定位元素位置
- 减少equals()的调用次数
- 提高性能
equals()的作用:
- 精确判断元素是否相等
- 处理hashCode()冲突的情况
两者缺一不可:
- 只有hashCode():无法处理冲突
- 只有equals():性能太差
1.2.2 重写equals()必须重写hashCode()
为什么必须重写?
Java规范要求:
- 如果两个对象equals()返回true,它们的hashCode()必须相同
- 如果两个对象equals()返回false,它们的hashCode()可以相同(哈希冲突)
在HashSet中的问题:
// 错误示例:只重写equals(),不重写hashCode()
public class Student {
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}
// 没有重写hashCode()
}
// 使用HashSet
Set<Student> set = new HashSet<>();
Student s1 = new Student("张三");
Student s2 = new Student("张三");
set.add(s1);
set.add(s2); // 会添加成功!因为hashCode()不同
// 问题:s1和s2的equals()返回true,但hashCode()不同
// HashSet会认为它们是不同的元素,导致重复
正确做法:
public class Student {
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name); // 必须重写hashCode()
}
}
hashCode()和equals()的契约
契约要求:
- 一致性: 同一对象的hashCode()在程序运行期间必须保持一致
- 相等性: 如果equals()返回true,hashCode()必须相同
- 不等性: 如果equals()返回false,hashCode()可以相同(哈希冲突)
违反契约的后果:
- HashSet无法正确判断元素是否重复
- 可能导致重复元素
- 无法通过contains()正确查找
1.3 核心方法实现
1.3.1 add()方法
实现原理
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
执行流程:
- 调用HashMap的put()方法
- 将元素作为key,PRESENT作为value
- 如果put()返回null,说明元素不存在,添加成功
- 如果put()返回非null,说明元素已存在,添加失败
返回值:
- true:元素不存在,添加成功
- false:元素已存在,添加失败
HashMap的put()方法
HashSet的add()实际上调用了HashMap的put()方法:
// HashMap的put()方法
public V put(K key, V value) {
// 1. 计算hash值
int hash = hash(key);
// 2. 定位数组下标
int index = (n - 1) & hash;
// 3. 检查位置是否为空
if (table[index] == null) {
// 直接插入
table[index] = new Node<>(hash, key, value, null);
return null; // 返回null表示新插入
}
// 4. 处理哈希冲突
// 遍历链表/红黑树,查找是否已存在
// 如果找到相同的key,更新value,返回旧value
// 如果没找到,插入新节点,返回null
}
1.3.2 remove()方法
实现原理
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
执行流程:
- 调用HashMap的remove()方法
- 如果remove()返回PRESENT,说明元素存在,删除成功
- 如果remove()返回null,说明元素不存在,删除失败
返回值:
- true:元素存在,删除成功
- false:元素不存在,删除失败
1.3.3 contains()方法
实现原理
public boolean contains(Object o) {
return map.containsKey(o);
}
执行流程:
- 调用HashMap的containsKey()方法
- 通过hashCode()快速定位
- 在链表/红黑树中查找
- 使用equals()比较
时间复杂度:
- 最好情况:O(1)
- 平均情况:O(1)
- 最坏情况:O(log n)(红黑树)
1.3.4 size()和isEmpty()方法
size()方法
public int size() {
return map.size();
}
特点:
- 直接返回HashMap的size
- 时间复杂度O(1)
isEmpty()方法
public boolean isEmpty() {
return map.isEmpty();
}
特点:
- 直接返回HashMap的isEmpty()
- 时间复杂度O(1)
1.3.5 iterator()方法
实现原理
public Iterator<E> iterator() {
return map.keySet().iterator();
}
特点:
- 返回HashMap的keySet()的迭代器
- 遍历所有元素(key)
- 不保证顺序
1.4 HashSet的常见陷阱
1.4.1 可变对象作为元素
问题示例
Set<List<String>> set = new HashSet<>();
List<String> list = new ArrayList<>();
list.add("A");
set.add(list);
list.add("B"); // 修改元素
// 现在set.contains(list)可能返回false
// 因为hashCode()改变了
问题:
- 修改元素后,hashCode()改变
- 无法通过contains()找到
- 可能导致内存泄漏
解决方案:
- 使用不可变对象
- 如果必须使用可变对象,确保不修改
1.4.2 只重写equals()不重写hashCode()
问题示例
// 只重写equals()
public class Student {
private String name;
@Override
public boolean equals(Object o) {
// ...
}
// 没有重写hashCode()
}
Set<Student> set = new HashSet<>();
Student s1 = new Student("张三");
Student s2 = new Student("张三");
set.add(s1);
set.add(s2); // 会添加成功!导致重复
问题:
- equals()返回true,但hashCode()不同
- HashSet认为它们是不同的元素
- 导致重复元素
解决方案:
- 同时重写equals()和hashCode()
- 保证equals()返回true时,hashCode()相同
1.4.3 null值处理
HashSet允许null值
Set<String> set = new HashSet<>();
set.add(null); // 允许
set.add(null); // 不会重复添加
System.out.println(set.size()); // 1
特点:
- HashSet允许一个null值
- 多次添加null,只保留一个
- 因为HashMap允许一个null key
📊 本章总结
核心要点:
- HashSet底层基于HashMap实现
- 元素作为HashMap的key存储,value统一为PRESENT
- 通过hashCode() + equals()保证元素唯一性
- 重写equals()必须重写hashCode()
- 允许一个null值
关键记忆:
- HashSet = HashMap的keySet()
- 唯一性 = hashCode() + equals()
- 必须同时重写equals()和hashCode()
第2章:LinkedHashSet深度剖析
2.1 LinkedHashSet基础原理
2.1.1 继承HashSet
继承关系
public class LinkedHashSet<E> extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
特点:
- 继承HashSet,拥有HashSet的所有功能
- 在此基础上增加了顺序维护
2.1.2 底层基于LinkedHashMap
核心数据结构
LinkedHashSet的底层使用LinkedHashMap实现:
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true); // 调用HashSet的构造函数
}
// HashSet的构造函数
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
特点:
- 底层使用LinkedHashMap
- LinkedHashMap在HashMap基础上增加双向链表
- 维护插入顺序
结构示意
LinkedHashSet
↓
LinkedHashMap
├─ HashMap结构(快速查找)
└─ 双向链表(维护顺序)
2.2 顺序维护机制
2.2.1 插入顺序
特点
LinkedHashSet按照插入顺序维护元素:
Set<String> set = new LinkedHashSet<>();
set.add("C");
set.add("A");
set.add("B");
// 迭代顺序:C -> A -> B(插入顺序)
for (String s : set) {
System.out.println(s); // C, A, B
}
优势:
- 保持插入顺序
- 适合需要顺序的场景
- 性能与HashSet相近
2.2.2 双向链表维护
实现原理
LinkedHashSet通过LinkedHashMap的双向链表维护顺序:
head ← → Node1 ← → Node2 ← → Node3 ← → tail
维护过程:
- 插入时:添加到链表尾部
- 删除时:从链表中移除
- 迭代时:按照链表顺序遍历
2.3 核心方法实现
2.3.1 add()方法
实现原理
// LinkedHashSet的add()方法继承自HashSet
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
// 底层调用LinkedHashMap的put()
// LinkedHashMap会维护双向链表
特点:
- 调用LinkedHashMap的put()
- 自动维护双向链表
- 保持插入顺序
2.3.2 iterator()方法
实现原理
// LinkedHashSet的iterator()继承自HashSet
public Iterator<E> iterator() {
return map.keySet().iterator();
}
// LinkedHashMap的keySet()返回有序的Set
// 迭代器按照插入顺序遍历
特点:
- 按照插入顺序遍历
- 性能与HashSet相近
- 适合需要顺序的场景
📊 本章总结
核心要点:
- LinkedHashSet继承HashSet
- 底层基于LinkedHashMap实现
- 维护元素的插入顺序
- 性能与HashSet相近
使用场景:
- 需要保持插入顺序
- 需要快速查找
- 需要去重
第3章:TreeSet深度剖析
3.1 TreeSet基础原理
3.1.1 底层基于TreeMap
核心数据结构
TreeSet的底层使用TreeMap实现:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
// 核心:底层使用TreeMap存储
private transient NavigableMap<E,Object> m;
// 虚拟值
private static final Object PRESENT = new Object();
}
特点:
- TreeSet内部维护一个TreeMap
- 所有元素作为TreeMap的key存储
- value统一使用PRESENT常量
3.1.2 红黑树实现
数据结构
TreeSet使用红黑树作为底层数据结构:
红黑树特点:
- 自平衡二叉搜索树
- 保证最坏情况下的查找性能为O(log n)
- 插入、删除、查找都是O(log n)
结构示意:
Root
/ \
Left Right
/ \ / \
... ... ... ...
3.2 排序机制
3.2.1 自然排序(Comparable)
特点
如果元素实现了Comparable接口,使用自然排序:
// String实现了Comparable接口
Set<String> set = new TreeSet<>();
set.add("C");
set.add("A");
set.add("B");
// 自动按自然顺序排序:{A, B, C}
System.out.println(set); // [A, B, C]
要求:
- 元素必须实现Comparable接口
- 或者提供Comparator
3.2.2 定制排序(Comparator)
特点
可以通过Comparator指定排序规则:
// 按字符串长度排序
Set<String> set = new TreeSet<>(
Comparator.comparing(String::length)
);
set.add("AAA");
set.add("B");
set.add("CC");
// 排序结果:{B, CC, AAA}(按长度)
System.out.println(set);
使用场景:
- 元素没有实现Comparable
- 需要自定义排序规则
3.2.3 排序规则的优先级
- 如果提供了Comparator: 使用Comparator
- 如果元素实现了Comparable: 使用自然排序
- 否则: 抛出ClassCastException
3.3 核心方法实现
3.3.1 add()方法
实现原理
public boolean add(E e) {
return m.put(e, PRESENT) == null;
}
执行流程:
- 调用TreeMap的put()方法
- TreeMap在红黑树中查找位置
- 如果元素已存在,返回false
- 如果元素不存在,插入并返回true
时间复杂度: O(log n)
3.3.2 contains()方法
实现原理
public boolean contains(Object o) {
return m.containsKey(o);
}
执行流程:
- 调用TreeMap的containsKey()方法
- 在红黑树中查找
- 使用Comparator或Comparable比较
时间复杂度: O(log n)
3.3.3 导航方法
first()和last()
public E first() {
return m.firstKey(); // 返回最小的元素
}
public E last() {
return m.lastKey(); // 返回最大的元素
}
ceiling()和floor()
public E ceiling(E e) {
return m.ceilingKey(e); // 大于等于e的最小元素
}
public E floor(E e) {
return m.floorKey(e); // 小于等于e的最大元素
}
higher()和lower()
public E higher(E e) {
return m.higherKey(e); // 大于e的最小元素
}
public E lower(E e) {
return m.lowerKey(e); // 小于e的最大元素
}
📊 本章总结
核心要点:
- TreeSet底层基于TreeMap实现
- 使用红黑树存储,保证O(log n)性能
- 支持自然排序和定制排序
- 提供丰富的导航方法
使用场景:
- 需要有序
- 需要范围查询
- 需要导航方法
第4章:Set集合对比与选择
4.1 三者对比
4.1.1 数据结构对比
| Set实现 | 底层数据结构 | 有序性 | 时间复杂度 |
|---|---|---|---|
| HashSet | HashMap | 无序 | O(1)平均 |
| LinkedHashSet | LinkedHashMap | 插入顺序 | O(1)平均 |
| TreeSet | TreeMap(红黑树) | 自然顺序 | O(log n) |
4.1.2 功能对比
HashSet
特点:
- 无序
- 性能最好
- 适合大多数场景
使用场景:
- 只需要去重
- 不需要顺序
- 性能要求高
LinkedHashSet
特点:
- 保持插入顺序
- 性能与HashSet相近
- 适合需要顺序的场景
使用场景:
- 需要保持插入顺序
- 需要快速查找
- 需要去重
TreeSet
特点:
- 按键排序
- 性能O(log n)
- 提供导航方法
使用场景:
- 需要有序
- 需要范围查询
- 需要导航方法
4.1.3 性能对比
插入性能
HashSet ≈ LinkedHashSet > TreeSet
原因:
- HashSet和LinkedHashSet:O(1)平均
- TreeSet:O(log n)
查找性能
HashSet ≈ LinkedHashSet > TreeSet
原因:
- HashSet和LinkedHashSet:O(1)平均
- TreeSet:O(log n)
内存占用
HashSet < LinkedHashSet < TreeSet
原因:
- HashSet:只存储元素
- LinkedHashSet:额外维护双向链表
- TreeSet:红黑树结构更复杂
4.2 选择指南
4.2.1 根据需求选择
选择流程图
需要Set?
↓
需要有序?
├─ 是 → 需要排序?
│ ├─ 是 → TreeSet
│ └─ 否 → LinkedHashSet(插入顺序)
└─ 否 → HashSet
4.2.2 具体场景
场景1:只需要去重
选择: HashSet
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // 不会重复
场景2:需要保持插入顺序
选择: LinkedHashSet
Set<String> set = new LinkedHashSet<>();
set.add("C");
set.add("A");
set.add("B");
// 顺序:C, A, B
场景3:需要排序
选择: TreeSet
Set<String> set = new TreeSet<>();
set.add("C");
set.add("A");
set.add("B");
// 顺序:A, B, C(自然排序)
场景4:需要范围查询
选择: TreeSet
TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(20);
set.add(30);
set.ceiling(15); // 20(大于等于15的最小值)
set.floor(25); // 20(小于等于25的最大值)
4.3 EnumSet简介
4.3.1 什么是EnumSet?
定义
EnumSet是专门为枚举类型设计的Set实现:
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
implements Cloneable, java.io.Serializable
特点:
- 只能存储枚举类型
- 使用位向量实现
- 性能极高
4.3.2 位向量实现
实现原理
EnumSet使用位向量(bit vector)实现:
// 假设有枚举
enum Color { RED, GREEN, BLUE }
// EnumSet内部使用long存储
// 每一位代表一个枚举值
// 例如:101 表示 RED和BLUE存在,GREEN不存在
优势:
- 内存占用极小
- 操作速度极快
- 适合枚举类型
4.3.3 使用示例
基本使用
enum Color { RED, GREEN, BLUE }
// 创建EnumSet
EnumSet<Color> set = EnumSet.of(Color.RED, Color.GREEN);
// 添加元素
set.add(Color.BLUE);
// 检查包含
set.contains(Color.RED); // true
常用方法
// 创建包含所有枚举值的Set
EnumSet<Color> all = EnumSet.allOf(Color.class);
// 创建空Set
EnumSet<Color> none = EnumSet.noneOf(Color.class);
// 创建指定范围的Set
EnumSet<Color> range = EnumSet.range(Color.RED, Color.BLUE);
4.3.4 适用场景
适用场景:
- 存储枚举类型
- 需要高性能
- 内存占用要求低
性能特点:
- 插入、删除、查找都是O(1)
- 内存占用极小
- 性能优于HashSet
📊 本章总结
核心要点:
- HashSet:无序,性能最好
- LinkedHashSet:插入顺序,性能与HashSet相近
- TreeSet:自然顺序,性能O(log n)
- EnumSet:枚举专用,性能极高
选择建议:
- 一般场景:HashSet
- 需要顺序:LinkedHashSet
- 需要排序:TreeSet
- 枚举类型:EnumSet
第5章:Set集合高频面试题精选
5.1 HashSet核心面试题
面试题1:HashSet是如何保证元素不重复的?
答案:
HashSet通过**hashCode() + equals()**双重检查来保证元素唯一性:
- 计算hashCode(): 快速定位元素位置
- 定位数组下标: 使用hashCode()计算位置
- 检查位置: 如果位置为空,直接添加
- 处理冲突: 如果位置不为空,遍历链表/红黑树
- equals()比较: 如果hashCode()相同,使用equals()精确比较
- 判断结果: 如果equals()返回true,不添加;否则添加
关键点:
- hashCode()用于快速定位
- equals()用于精确判断
- 两者缺一不可
面试题2:HashSet的底层是什么?它的add方法实际上调用了什么?
答案:
底层实现:
- HashSet底层基于HashMap实现
- 元素作为HashMap的key存储
- value统一使用PRESENT常量
add()方法:
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
实际调用:
- add()方法调用HashMap的put()方法
- put()方法执行哈希计算、冲突处理等逻辑
- 如果put()返回null,说明元素不存在,添加成功
面试题3:为什么重写equals()必须重写hashCode()?在HashSet中会引发什么问题?
答案:
为什么必须重写?
Java规范要求:
- 如果两个对象equals()返回true,它们的hashCode()必须相同
- 违反这个规则会导致HashSet无法正确判断元素是否重复
在HashSet中的问题:
// 错误示例:只重写equals()
public class Student {
private String name;
@Override
public boolean equals(Object o) {
// ...
}
// 没有重写hashCode()
}
Set<Student> set = new HashSet<>();
Student s1 = new Student("张三");
Student s2 = new Student("张三");
set.add(s1);
set.add(s2); // 会添加成功!导致重复
// 问题:
// s1和s2的equals()返回true,但hashCode()不同
// HashSet认为它们是不同的元素,导致重复
正确做法:
- 同时重写equals()和hashCode()
- 保证equals()返回true时,hashCode()相同
面试题4:HashSet和HashMap的关系是什么?
答案:
关系:
- HashSet底层基于HashMap实现
- HashSet = HashMap的keySet()
- HashSet的所有元素作为HashMap的key存储
代码证明:
// HashSet内部
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
面试题5:HashSet允许null值吗?允许几个?
答案:
允许:
- HashSet允许一个null值
- 多次添加null,只保留一个
原因:
- HashSet底层使用HashMap
- HashMap允许一个null key
- 所以HashSet允许一个null值
示例:
Set<String> set = new HashSet<>();
set.add(null);
set.add(null);
System.out.println(set.size()); // 1
面试题6:HashSet的迭代顺序是什么?
答案:
无序:
- HashSet不保证元素的迭代顺序
- 顺序可能因哈希值、扩容等因素改变
- 如果需要顺序,使用LinkedHashSet或TreeSet
面试题7:HashSet的性能如何?时间复杂度是多少?
答案:
时间复杂度:
- 插入:O(1)平均,O(log n)最坏(红黑树)
- 删除:O(1)平均,O(log n)最坏
- 查找:O(1)平均,O(log n)最坏
性能特点:
- 平均情况下性能优秀
- 哈希冲突严重时性能下降
- JDK8引入红黑树优化,最坏情况O(log n)
面试题8:HashSet是线程安全的吗?
答案:
不是:
- HashSet不是线程安全的
- 多线程场景需要使用Collections.synchronizedSet()或ConcurrentHashMap
线程安全方案:
// 方案1:使用Collections.synchronizedSet()
Set<String> set = Collections.synchronizedSet(new HashSet<>());
// 方案2:使用ConcurrentHashMap的keySet()
Set<String> set = ConcurrentHashMap.newKeySet();
5.2 LinkedHashSet面试题
面试题9:LinkedHashSet和HashSet的主要区别是什么?
答案:
主要区别:
| 特性 | HashSet | LinkedHashSet |
|---|---|---|
| 有序性 | 无序 | 插入顺序 |
| 底层实现 | HashMap | LinkedHashMap |
| 性能 | O(1)平均 | O(1)平均 |
| 内存占用 | 较小 | 稍大(维护链表) |
核心区别:
- LinkedHashSet维护插入顺序
- HashSet不保证顺序
面试题10:LinkedHashSet是如何维护插入顺序的?
答案:
实现方式:
- LinkedHashSet底层使用LinkedHashMap
- LinkedHashMap在HashMap基础上增加双向链表
- 插入时添加到链表尾部
- 迭代时按照链表顺序遍历
结构:
LinkedHashSet
↓
LinkedHashMap
├─ HashMap结构(快速查找)
└─ 双向链表(维护顺序)
面试题11:LinkedHashSet的性能如何?与HashSet相比如何?
答案:
性能对比:
- 插入性能:与HashSet相近(O(1)平均)
- 查找性能:与HashSet相近(O(1)平均)
- 内存占用:稍大于HashSet(维护链表)
结论:
- 性能与HashSet相近
- 只是多维护一个链表,开销很小
- 适合需要顺序的场景
面试题12:什么场景下应该使用LinkedHashSet?
答案:
适用场景:
- 需要保持插入顺序
- 需要快速查找
- 需要去重
- 需要记录访问顺序(LRU缓存)
示例:
// 记录用户访问顺序
Set<String> visitedPages = new LinkedHashSet<>();
visitedPages.add("首页");
visitedPages.add("产品页");
visitedPages.add("详情页");
// 顺序:首页 -> 产品页 -> 详情页
5.3 TreeSet面试题
面试题13:TreeSet的底层数据结构是什么?时间复杂度是多少?
答案:
数据结构:
- TreeSet底层基于TreeMap实现
- TreeMap使用红黑树(自平衡二叉搜索树)
时间复杂度:
- 插入:O(log n)
- 删除:O(log n)
- 查找:O(log n)
特点:
- 保证最坏情况下的性能
- 比HashSet慢,但保证有序
面试题14:TreeSet的排序规则和TreeMap一致吗?
答案:
一致:
- TreeSet底层使用TreeMap
- 排序规则完全一致
- 都支持自然排序和定制排序
排序规则:
- 如果提供了Comparator:使用Comparator
- 如果元素实现了Comparable:使用自然排序
- 否则:抛出ClassCastException
面试题15:TreeSet和HashSet的主要区别是什么?
答案:
主要区别:
| 特性 | HashSet | TreeSet |
|---|---|---|
| 数据结构 | HashMap | TreeMap(红黑树) |
| 有序性 | 无序 | 有序(自然顺序) |
| 时间复杂度 | O(1)平均 | O(log n) |
| 性能 | 更快 | 较慢 |
| 导航方法 | 无 | 有(ceiling、floor等) |
选择建议:
- 只需要去重:HashSet
- 需要有序:TreeSet
面试题16:TreeSet的key可以为null吗?
答案:
不可以:
- TreeSet的key不能为null
- 会抛出NullPointerException
原因:
- TreeSet底层使用TreeMap
- TreeMap的key不能为null
- 因为需要比较,null无法比较
面试题17:TreeSet是线程安全的吗?
答案:
不是:
- TreeSet不是线程安全的
- 多线程场景需要使用Collections.synchronizedSet()或ConcurrentSkipListSet
线程安全方案:
// 方案1:使用Collections.synchronizedSet()
Set<String> set = Collections.synchronizedSet(new TreeSet<>());
// 方案2:使用ConcurrentSkipListSet(推荐)
Set<String> set = new ConcurrentSkipListSet<>();
面试题18:什么场景下应该使用TreeSet?
答案:
适用场景:
- 需要有序
- 需要范围查询
- 需要导航方法(ceiling、floor等)
- 需要排序后的数据
示例:
// 需要排序的成绩列表
TreeSet<Integer> scores = new TreeSet<>();
scores.add(85);
scores.add(92);
scores.add(78);
// 自动排序:78, 85, 92
// 范围查询
scores.ceiling(80); // 85(大于等于80的最小值)
scores.floor(90); // 85(小于等于90的最大值)
5.4 Set集合综合面试题
面试题19:HashSet、LinkedHashSet、TreeSet三者的区别是什么?
答案:
对比表:
| 特性 | HashSet | LinkedHashSet | TreeSet |
|---|---|---|---|
| 底层实现 | HashMap | LinkedHashMap | TreeMap |
| 有序性 | 无序 | 插入顺序 | 自然顺序 |
| 时间复杂度 | O(1)平均 | O(1)平均 | O(log n) |
| 性能 | 最快 | 较快 | 较慢 |
| 内存占用 | 较小 | 中等 | 较大 |
| null值 | 允许一个 | 允许一个 | 不允许 |
| 线程安全 | 否 | 否 | 否 |
核心区别:
- HashSet:无序,性能最好
- LinkedHashSet:插入顺序,性能与HashSet相近
- TreeSet:自然顺序,性能O(log n)
面试题20:如何根据"是否需要排序"、"是否需要保持插入顺序"来选择Set的实现?
答案:
选择指南:
需要Set?
↓
需要有序?
├─ 是 → 需要排序?
│ ├─ 是 → TreeSet
│ └─ 否 → LinkedHashSet(插入顺序)
└─ 否 → HashSet
具体场景:
- 只需要去重,不需要顺序: HashSet
- 需要保持插入顺序: LinkedHashSet
- 需要排序: TreeSet
- 需要范围查询: TreeSet
- 需要导航方法: TreeSet
面试题21:Set集合的迭代器是fail-fast还是fail-safe?
答案:
fail-fast:
- HashSet、LinkedHashSet、TreeSet的迭代器都是fail-fast
- 迭代过程中修改集合会抛出ConcurrentModificationException
示例:
Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
Iterator<String> it = set.iterator();
set.add("C"); // 修改集合
it.next(); // 抛出ConcurrentModificationException
面试题22:Set集合的contains()方法如何实现?
答案:
HashSet:
- 调用HashMap的containsKey()
- 通过hashCode()快速定位
- 使用equals()比较
LinkedHashSet:
- 与HashSet相同(底层使用LinkedHashMap)
TreeSet:
- 调用TreeMap的containsKey()
- 在红黑树中查找
- 使用Comparator或Comparable比较
面试题23:Set集合的size()方法时间复杂度是多少?
答案:
都是O(1):
- HashSet:直接返回HashMap的size
- LinkedHashSet:直接返回LinkedHashMap的size
- TreeSet:直接返回TreeMap的size
原因:
- 底层Map都维护了size字段
- 不需要遍历计算
面试题24:Set集合的addAll()方法如何实现?
答案:
实现方式:
- 遍历参数集合
- 对每个元素调用add()方法
- 如果add()返回true,说明是新元素
时间复杂度:
- O(n),n是参数集合的大小
面试题25:EnumSet的适用场景是什么?它为什么性能极高?
答案:
适用场景:
- 存储枚举类型
- 需要高性能
- 内存占用要求低
为什么性能极高?
- 位向量实现: 使用long存储,每一位代表一个枚举值
- 位运算: 所有操作都是位运算,速度极快
- 内存占用小: 只占用很少的内存
- O(1)操作: 插入、删除、查找都是O(1)
性能对比:
- EnumSet > HashSet > TreeSet
面试题26:Set集合的removeAll()和retainAll()方法有什么区别?
答案:
removeAll():
- 移除所有在参数集合中的元素
- 保留不在参数集合中的元素
retainAll():
- 保留所有在参数集合中的元素
- 移除不在参数集合中的元素
示例:
Set<String> set1 = new HashSet<>(Arrays.asList("A", "B", "C"));
Set<String> set2 = new HashSet<>(Arrays.asList("B", "C"));
set1.removeAll(set2); // set1 = {A}
set1.retainAll(set2); // set1 = {B, C}
面试题27:如何实现一个线程安全的Set?
答案:
方案1:使用Collections.synchronizedSet()
Set<String> set = Collections.synchronizedSet(new HashSet<>());
方案2:使用ConcurrentHashMap的keySet()
Set<String> set = ConcurrentHashMap.newKeySet();
方案3:使用ConcurrentSkipListSet(有序)
Set<String> set = new ConcurrentSkipListSet<>();
面试题28:Set集合的equals()方法如何实现?
答案:
实现逻辑:
- 检查是否是Set类型
- 检查size是否相同
- 检查是否包含所有元素(使用contains())
时间复杂度: O(n)
面试题29:Set集合的hashCode()方法如何实现?
答案:
实现逻辑:
- 将所有元素的hashCode()相加
- 保证相等的Set有相同的hashCode()
注意:
- Set的hashCode()等于所有元素hashCode()之和
- 顺序不影响hashCode()
面试题30:如何优化Set集合的性能?
答案:
优化策略:
-
选择合适的实现:
- 一般场景:HashSet
- 需要顺序:LinkedHashSet
- 需要排序:TreeSet
- 枚举类型:EnumSet
-
预分配容量(HashSet、LinkedHashSet):
Set<String> set = new HashSet<>(expectedSize); -
实现好的hashCode():
- 减少哈希冲突
- 提高性能
-
使用不可变对象:
- 避免修改元素导致的问题
文档完成!
本文档全面覆盖了Java Set集合框架的所有核心知识点,包含30道大厂高频面试题,适合系统学习和面试准备。