Java集合篇———Set

24 阅读21分钟

第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()的契约

契约要求:

  1. 一致性: 同一对象的hashCode()在程序运行期间必须保持一致
  2. 相等性: 如果equals()返回true,hashCode()必须相同
  3. 不等性: 如果equals()返回false,hashCode()可以相同(哈希冲突)

违反契约的后果:

  • HashSet无法正确判断元素是否重复
  • 可能导致重复元素
  • 无法通过contains()正确查找

1.3 核心方法实现

1.3.1 add()方法

实现原理
public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

执行流程:

  1. 调用HashMap的put()方法
  2. 将元素作为key,PRESENT作为value
  3. 如果put()返回null,说明元素不存在,添加成功
  4. 如果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;
}

执行流程:

  1. 调用HashMap的remove()方法
  2. 如果remove()返回PRESENT,说明元素存在,删除成功
  3. 如果remove()返回null,说明元素不存在,删除失败

返回值:

  • true:元素存在,删除成功
  • false:元素不存在,删除失败

1.3.3 contains()方法

实现原理
public boolean contains(Object o) {
    return map.containsKey(o);
}

执行流程:

  1. 调用HashMap的containsKey()方法
  2. 通过hashCode()快速定位
  3. 在链表/红黑树中查找
  4. 使用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

📊 本章总结

核心要点:

  1. HashSet底层基于HashMap实现
  2. 元素作为HashMap的key存储,value统一为PRESENT
  3. 通过hashCode() + equals()保证元素唯一性
  4. 重写equals()必须重写hashCode()
  5. 允许一个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相近
  • 适合需要顺序的场景

📊 本章总结

核心要点:

  1. LinkedHashSet继承HashSet
  2. 底层基于LinkedHashMap实现
  3. 维护元素的插入顺序
  4. 性能与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 排序规则的优先级

  1. 如果提供了Comparator: 使用Comparator
  2. 如果元素实现了Comparable: 使用自然排序
  3. 否则: 抛出ClassCastException

3.3 核心方法实现

3.3.1 add()方法

实现原理
public boolean add(E e) {
    return m.put(e, PRESENT) == null;
}

执行流程:

  1. 调用TreeMap的put()方法
  2. TreeMap在红黑树中查找位置
  3. 如果元素已存在,返回false
  4. 如果元素不存在,插入并返回true

时间复杂度: O(log n)

3.3.2 contains()方法

实现原理
public boolean contains(Object o) {
    return m.containsKey(o);
}

执行流程:

  1. 调用TreeMap的containsKey()方法
  2. 在红黑树中查找
  3. 使用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的最大元素
}

📊 本章总结

核心要点:

  1. TreeSet底层基于TreeMap实现
  2. 使用红黑树存储,保证O(log n)性能
  3. 支持自然排序和定制排序
  4. 提供丰富的导航方法

使用场景:

  • 需要有序
  • 需要范围查询
  • 需要导航方法

第4章:Set集合对比与选择

4.1 三者对比

4.1.1 数据结构对比

Set实现底层数据结构有序性时间复杂度
HashSetHashMap无序O(1)平均
LinkedHashSetLinkedHashMap插入顺序O(1)平均
TreeSetTreeMap(红黑树)自然顺序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

📊 本章总结

核心要点:

  1. HashSet:无序,性能最好
  2. LinkedHashSet:插入顺序,性能与HashSet相近
  3. TreeSet:自然顺序,性能O(log n)
  4. EnumSet:枚举专用,性能极高

选择建议:

  • 一般场景:HashSet
  • 需要顺序:LinkedHashSet
  • 需要排序:TreeSet
  • 枚举类型:EnumSet

第5章:Set集合高频面试题精选

5.1 HashSet核心面试题

面试题1:HashSet是如何保证元素不重复的?

答案:

HashSet通过**hashCode() + equals()**双重检查来保证元素唯一性:

  1. 计算hashCode(): 快速定位元素位置
  2. 定位数组下标: 使用hashCode()计算位置
  3. 检查位置: 如果位置为空,直接添加
  4. 处理冲突: 如果位置不为空,遍历链表/红黑树
  5. equals()比较: 如果hashCode()相同,使用equals()精确比较
  6. 判断结果: 如果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的主要区别是什么?

答案:

主要区别:

特性HashSetLinkedHashSet
有序性无序插入顺序
底层实现HashMapLinkedHashMap
性能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
  • 排序规则完全一致
  • 都支持自然排序和定制排序

排序规则:

  1. 如果提供了Comparator:使用Comparator
  2. 如果元素实现了Comparable:使用自然排序
  3. 否则:抛出ClassCastException
面试题15:TreeSet和HashSet的主要区别是什么?

答案:

主要区别:

特性HashSetTreeSet
数据结构HashMapTreeMap(红黑树)
有序性无序有序(自然顺序)
时间复杂度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三者的区别是什么?

答案:

对比表:

特性HashSetLinkedHashSetTreeSet
底层实现HashMapLinkedHashMapTreeMap
有序性无序插入顺序自然顺序
时间复杂度O(1)平均O(1)平均O(log n)
性能最快较快较慢
内存占用较小中等较大
null值允许一个允许一个不允许
线程安全

核心区别:

  • HashSet:无序,性能最好
  • LinkedHashSet:插入顺序,性能与HashSet相近
  • TreeSet:自然顺序,性能O(log n)
面试题20:如何根据"是否需要排序"、"是否需要保持插入顺序"来选择Set的实现?

答案:

选择指南:

需要Set?
    ↓
需要有序?
    ├─ 是 → 需要排序?
    │       ├─ 是 → TreeSet
    │       └─ 否 → LinkedHashSet(插入顺序)
    └─ 否 → HashSet

具体场景:

  1. 只需要去重,不需要顺序: HashSet
  2. 需要保持插入顺序: LinkedHashSet
  3. 需要排序: TreeSet
  4. 需要范围查询: TreeSet
  5. 需要导航方法: 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的适用场景是什么?它为什么性能极高?

答案:

适用场景:

  • 存储枚举类型
  • 需要高性能
  • 内存占用要求低

为什么性能极高?

  1. 位向量实现: 使用long存储,每一位代表一个枚举值
  2. 位运算: 所有操作都是位运算,速度极快
  3. 内存占用小: 只占用很少的内存
  4. 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()方法如何实现?

答案:

实现逻辑:

  1. 检查是否是Set类型
  2. 检查size是否相同
  3. 检查是否包含所有元素(使用contains())

时间复杂度: O(n)

面试题29:Set集合的hashCode()方法如何实现?

答案:

实现逻辑:

  • 将所有元素的hashCode()相加
  • 保证相等的Set有相同的hashCode()

注意:

  • Set的hashCode()等于所有元素hashCode()之和
  • 顺序不影响hashCode()
面试题30:如何优化Set集合的性能?

答案:

优化策略:

  1. 选择合适的实现:

    • 一般场景:HashSet
    • 需要顺序:LinkedHashSet
    • 需要排序:TreeSet
    • 枚举类型:EnumSet
  2. 预分配容量(HashSet、LinkedHashSet):

    Set<String> set = new HashSet<>(expectedSize);
    
  3. 实现好的hashCode():

    • 减少哈希冲突
    • 提高性能
  4. 使用不可变对象:

    • 避免修改元素导致的问题

文档完成!

本文档全面覆盖了Java Set集合框架的所有核心知识点,包含30道大厂高频面试题,适合系统学习和面试准备。