java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java Set的实现类

Java集合框架之Set的实现类与实战使用详解

作者:星河耀银海

本文深入解析Java集合框架中的Set接口及其实现类,重点内容包括Set接口不可重复性和无序性的设计理念,HashSet基于哈希表的底层实现与去重机制,LinkedHashSet维护插入顺序的原理等,需要的朋友可以参考下

一、章节学习目标与重点

1.1 学习目标

1.2 学习重点

二、Set 接口核心特性与设计理念

💡 Set 作为 Java 集合框架的核心接口之一,继承自 Collection 接口,其最核心的设计理念是元素不可重复性和无序性(部分实现类支持有序):

// Set 接口核心方法(继承自 Collection)
public interface Set<E> extends Collection<E> {
    // 添加元素(若元素已存在则返回 false)
    boolean add(E e);
    // 批量添加元素
    boolean addAll(Collection<? extends E> c);
    // 删除元素
    boolean remove(Object o);
    // 判断是否包含元素
    boolean contains(Object o);
    // 获取迭代器
    Iterator<E> iterator();
    // 其他方法(size()、isEmpty()、clear() 等)
}

⚠️ 关键注意点:

  1. Set 的去重机制依赖元素的 equals() 方法,但为了提高查找效率,会先通过 hashCode() 方法计算哈希值,因此重写 equals() 方法时必须重写 hashCode() 方法,否则会导致去重失效
  2. Set 接口没有提供基于索引的访问方法(如 get(int index)),因为其设计初衷不强调元素的顺序访问
  3. 所有 Set 实现类均为非线程安全(除 ConcurrentSkipListSet 等并发实现类),多线程环境下需手动保证线程安全

2.1 Set与List接口核心区别对比

特性Set 接口List 接口
元素重复性不可重复(基于 equals())可重复
元素有序性无序(部分实现类除外)有序(插入顺序=遍历顺序)
索引访问支持不支持支持(get/set 等方法)
底层实现依赖哈希表/红黑树(去重/排序)动态数组/双向链表(访问)
核心用途去重、无序存储、排序存储有序存储、索引访问、重复元素存储

三、HashSet深度解析:哈希表实现的高效去重集合

3.1 底层数据结构与核心成员变量

💡 HashSet 是 Set 接口最常用的实现类,其底层基于哈希表(HashMap) 实现,利用 HashMap 的 key 不可重复特性实现 Set 的去重功能。本质上,HashSet 就是一个“精简版”的 HashMap——只使用 key 存储元素,value 固定为一个静态常量对象。

public class HashSet<E> extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable {
    // 底层存储核心:HashMap 对象
    private transient HashMap<E, Object> map;
    // 固定的 value 对象,所有 key 都映射到该对象
    private static final Object PRESENT = new Object();
    // 无参构造:创建空的 HashMap
    public HashSet() {
        map = new HashMap<>();
    }
    // 指定初始容量的构造函数
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    // 指定初始容量和负载因子的构造函数
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
}

哈希表结构示意图(基于 JDK 8,数组+链表/红黑树):

哈希表(HashMap)
┌─────────┬─────────┬─────────┬─────────┐
│ 桶 0    │ 桶 1    │ 桶 2    │ 桶 3    │
├─────────┼─────────┼─────────┼─────────┤
│ null    │ 链表    │ 红黑树  │ null    │
│         │ (A→B→C) │ (D→E→F) │         │
└─────────┴─────────┴─────────┴─────────┘
  ↑         ↑         ↑
  |         |         |
HashSet 元素:A、B、C、D、E、F(存储在 HashMap 的 key 中)

3.2 核心方法源码解析(基于HashMap委托实现)

HashSet 的所有核心方法均委托给底层的 HashMap 实现,因此理解 HashMap 的工作原理是掌握 HashSet 的关键。

add(E e) 方法:元素添加与去重机制

public boolean add(E e) {
    // 调用 HashMap 的 put 方法,key 为待添加元素,value 为 PRESENT
    // HashMap 的 put 方法返回 null 表示添加成功(key 不存在),返回旧 value 表示添加失败(key 已存在)
    return map.put(e, PRESENT) == null;
}

💡 去重机制核心流程:

1. 当调用 add(E e) 方法时,HashSet 会将元素 e 作为 key 传入 HashMap 的 put 方法

2. HashMap 首先调用 e 的 hashCode() 方法计算哈希值,根据哈希值确定元素在数组中的桶位置

3. 若桶位置为空,则直接创建节点存入,添加成功(返回 true)

4. 若桶位置不为空(发生哈希冲突),则通过 equals() 方法比较桶中已有元素与 e:

⚠️ 关键结论:HashSet 的去重依赖 hashCode() + equals() 方法的协同工作:

contains(Object o)方法:元素查找机制

public boolean contains(Object o) {
    // 委托给 HashMap 的 containsKey 方法
    return map.containsKey(o);
}

HashMap 的 containsKey 方法查找流程:

  1. 计算 o 的 hashCode() 得到哈希值,确定桶位置
  2. 遍历桶中的元素(链表或红黑树),通过 equals() 方法比较是否存在匹配元素
  3. 找到则返回 true,否则返回 false

💡 哈希表的查找效率极高,理想情况下(无哈希冲突)时间复杂度为 O(1),最坏情况下(所有元素哈希冲突,链表结构)时间复杂度为 O(n),红黑树结构下为 O(log n)

remove(Object o)方法:元素删除机制

public boolean remove(Object o) {
    // 委托给 HashMap 的 remove 方法,返回 PRESENT 表示删除成功
    return map.remove(o) == PRESENT;
}

删除流程与查找流程类似:先通过 hashCode() 定位桶位置,再通过 equals() 找到目标元素,最后删除该元素(并维护链表/红黑树结构)。

3.3 哈希冲突与解决策略

哈希冲突的产生

当两个不同元素的 hashCode() 方法返回相同的哈希值时,它们会被分配到哈希表的同一个桶中,这种情况称为哈希冲突。

例如:

class Person {
    private String name;
    private int age;
    // 仅重写 equals(),未重写 hashCode()
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
    // 省略构造函数和 getter/setter
}
// 测试:两个 equals() 相同但 hashCode() 不同的对象
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // false(Object 类的 hashCode() 基于对象地址)
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 2(去重失效!)

哈希冲突的解决策略(HashMap/HashSet实现)

  1. 链地址法(拉链法):将同一个桶中的冲突元素以链表形式存储,JDK 8 中当链表长度超过 8 且数组长度≥64 时,转为红黑树
  2. 扰动函数:对 hashCode() 的返回值进行二次哈希计算(JDK 8 简化为一次异或和无符号右移),减少哈希冲突的概率
  3. 动态扩容:当哈希表的负载因子(元素个数/数组容量)超过阈值(默认 0.75)时,数组容量扩容为原来的 2 倍,重新分配所有元素的桶位置

正确重写 hashCode() 与 equals() 方法

为避免哈希冲突导致的去重失效,必须遵循以下重写原则:

  1. 自反性:x.equals(x) 必须返回 true
  2. 对称性:若 x.equals(y) 为 true,则 y.equals(x) 也必须为 true
  3. 传递性:若 x.equals(y) 为 true 且 y.equals(z) 为 true,则 x.equals(z) 必须为 true
  4. 一致性:若 x 和 y 的equals() 比较所依赖的属性未变,则 x.equals(y) 的结果始终不变
  5. 哈希一致性:若 x.equals(y) 为 true,则 x.hashCode() 必须等于 y.hashCode();若 x.equals(y) 为 false,x.hashCode() 与 y.hashCode() 可以相等(但尽量不同,减少冲突)

正确重写示例:

class Person {
    private String name;
    private int age;
    // 构造函数、getter/setter 省略
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
    @Override
    public int hashCode() {
        // 基于 equals() 依赖的属性计算哈希值
        return Objects.hash(name, age);
    }
}
// 测试:去重生效
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
HashSet<Person> set = new HashSet<>();
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 1(正确去重)

3.4 HashSet性能分析与使用场景

3.4.1 性能特点

操作类型时间复杂度说明
add()/contains()/remove()O(1)(平均情况)无哈希冲突时效率最高,冲突严重时下降
遍历(iterator())O(n)需遍历所有桶和元素
扩容O(n)数组扩容时需重新哈希并迁移所有元素

3.4.2 关键性能参数

3.4.3 适用场景

3.4.4 性能优化技巧

💡 技巧 1:初始化时指定合理的初始容量,减少扩容次数。若已知元素数量为 N,推荐初始容量设置为 N / 负载因子 + 1(例如 N=1000,负载因子 0.75,初始容量=1000/0.75+1≈1334)

// 推荐:已知元素约 1000 个,指定初始容量 1334
HashSet<String> set = new HashSet<>(1334);

💡 技巧 2:避免使用哈希值易冲突的元素类型,或确保 hashCode() 方法的哈希分布均匀,减少冲突

💡 技巧 3:单线程环境使用 HashSet,多线程环境可使用 Collections.synchronizedSet(new HashSet<>())ConcurrentHashMap.newKeySet()(JUC 提供,性能更优)

四、LinkedHashSet深度解析:有序去重的哈希集合

4.1 底层数据结构与核心特性

💡 LinkedHashSet 继承自 HashSet,其底层基于哈希表(HashMap)+ 双向链表实现,在 HashSet 去重功能的基础上,额外保证了元素的插入顺序(遍历顺序与插入顺序一致)。

public class LinkedHashSet<E> extends HashSet<E>
        implements Set<E>, Cloneable, java.io.Serializable {
    // 构造函数:调用父类 HashSet 的构造函数,创建 LinkedHashMap 实例
    public LinkedHashSet() {
        super(16, .75f, true);
    }
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
    // 父类 HashSet 中对应的构造函数(包访问权限)
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
}

核心结构示意图:

哈希表(LinkedHashMap)
┌─────────┬─────────┬─────────┐
│ 桶 0    │ 桶 1    │ 桶 2    │
├─────────┼─────────┼─────────┤
│ A       │ B→C     │ D       │
└─────────┴─────────┴─────────┘
  ↑         ↑         ↑
  │         │         │
双向链表:A ←→ B ←→ C ←→ D(维护插入顺序)

4.2 核心特性与源码解析

4.2.1 有序性保障原理

LinkedHashMap 中的每个节点(Entry)除了包含 HashMap 的 key、value、hash、next 字段外,还新增了两个指针(before 和 after),用于构建双向链表:

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after; // 双向链表的前驱和后继指针
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

当调用 add(E e) 方法时,元素会同时被添加到哈希表和双向链表中:

  1. 哈希表部分:与 HashSet 逻辑一致,通过 hashCode() 和 equals() 保证去重
  2. 双向链表部分:新元素会被添加到双向链表的尾部,从而维护插入顺序

4.2.2 遍历效率与性能特点

LinkedHashSet 的遍历效率高于 HashSet,因为遍历是通过双向链表进行的,无需遍历哈希表中的空桶;而 HashSet 遍历需要遍历整个哈希表数组,包括空桶。

性能对比(以 100 万条元素为例):

操作类型HashSetLinkedHashSet
添加 100 万元素89ms102ms
查找单个元素0ms0ms
遍历 100 万元素12ms8ms

💡 结论:LinkedHashSet 的添加操作略慢于 HashSet(因为需要维护双向链表),但遍历效率更高;查找和删除效率与 HashSet 基本一致(均依赖哈希表)。

4.2.3 访问顺序模式(LinkedHashSet 扩展特性)

LinkedHashMap 支持两种顺序模式:

  1. 插入顺序(默认):遍历顺序与元素插入顺序一致
  2. 访问顺序:遍历顺序与元素最后访问时间一致(get() 或 put() 操作会将元素移到双向链表尾部)

虽然 LinkedHashSet 本身未直接提供访问顺序的构造函数,但可通过自定义 LinkedHashMap 实现:

// 自定义访问顺序的 LinkedHashSet
class AccessOrderLinkedHashSet<E> extends LinkedHashSet<E> {
    public AccessOrderLinkedHashSet(int initialCapacity, float loadFactor) {
        super(new LinkedHashMap<>(initialCapacity, loadFactor, true));
    }
}
// 测试访问顺序
public class TestAccessOrder {
    public static void main(String[] args) {
        AccessOrderLinkedHashSet<String> set = new AccessOrderLinkedHashSet<>(16, 0.75f);
        set.add("A");
        set.add("B");
        set.add("C");
        System.out.println("初始遍历:" + new ArrayList<>(set)); // [A, B, C](插入顺序)
        set.contains("B"); // 访问 B 元素
        System.out.println("访问 B 后遍历:" + new ArrayList<>(set)); // [A, C, B](B 移到尾部)
    }
}

4.3 LinkedHashSet 适用场景与注意事项

4.3.1 适用场景

4.3.2 注意事项

⚠️ 注意 1:LinkedHashSet 的内存占用高于 HashSet,因为每个节点需要额外存储 before 和 after 指针,因此在内存敏感场景需谨慎使用

⚠️ 注意 2:LinkedHashSet 的有序性是“插入顺序”,而非“自然顺序”或“自定义排序顺序”,若需要排序功能,需使用 TreeSet

⚠️ 注意 3:与 HashSet 一样,LinkedHashSet 非线程安全,多线程环境需手动同步

五、TreeSet 深度解析:基于红黑树的排序集合

5.1 底层数据结构与核心特性

💡 TreeSet 是 Set 接口的排序实现类,底层基于红黑树(TreeMap) 实现,其核心特性是元素有序性(自然排序或定制排序)和不可重复性。

public class TreeSet<E> extends AbstractSet<E>
        implements NavigableSet<E>, Cloneable, java.io.Serializable {
    // 底层存储核心:TreeMap 对象
    private transient NavigableMap<E, Object> m;
    // 固定的 value 对象,与 HashSet 类似
    private static final Object PRESENT = new Object();
    // 构造函数:创建 TreeMap 实例
    public TreeSet() {
        this(new TreeMap<>());
    }
    // 自定义比较器的构造函数
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    TreeSet(NavigableMap<E, Object> m) {
        this.m = m;
    }
}

红黑树结构示意图(有序二叉查找树):

        8(根节点,黑色)
      /   \
     3(红)  10(红)
    / \       \
   1(黑)5(黑) 14(黑)
      / \     /
     4(红)6(红)13(红)

红黑树的核心特性(保证树的平衡,提高查询效率):

  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色
  3. 所有叶子节点(NIL 节点)是黑色
  4. 若一个节点是红色,则其两个子节点都是黑色
  5. 从任意节点到其所有叶子节点的路径上,黑色节点的数量相同

5.2 排序机制:自然排序与定制排序

TreeSet 的有序性依赖于比较器(Comparator) 或元素自身实现的Comparable 接口,分为两种排序方式:

5.2.1 自然排序(默认)

若元素实现了 Comparable 接口,TreeSet 会调用元素的 compareTo() 方法进行排序,这就是自然排序。

常见的实现了 Comparable 接口的类:

示例:自定义类的自然排序

class Student implements Comparable<Student> {
    private String id;
    private int score;
    // 构造函数、getter/setter 省略
    // 按分数降序排序,分数相同按学号升序排序
    @Override
    public int compareTo(Student o) {
        if (this.score != o.score) {
            return o.score - this.score; // 降序(o.score - this.score)
        }
        return this.id.compareTo(o.id); // 升序
    }
    @Override
    public String toString() {
        return "Student{id='" + id + "', score=" + score + "}";
    }
}
// 测试自然排序
public class TreeSetNaturalSortTest {
    public static void main(String[] args) {
        TreeSet<Student> set = new TreeSet<>();
        set.add(new Student("001", 90));
        set.add(new Student("002", 85));
        set.add(new Student("003", 90));
        set.add(new Student("004", 95));
        // 遍历:按分数降序,分数相同按学号升序
        for (Student s : set) {
            System.out.println(s);
        }
        // 输出结果:
        // Student{id='004', score=95}
        // Student{id='001', score=90}
        // Student{id='003', score=90}
        // Student{id='002', score=85}
    }
}

5.2.2 定制排序(自定义比较器)

若元素未实现 Comparable 接口,或需要自定义排序规则,可通过 TreeSet 的构造函数传入 Comparator 接口实现类,这就是定制排序。

示例:基于 Comparator 的定制排序

class Employee {
    private String name;
    private int age;
    // 构造函数、getter/setter、toString 省略
}
// 测试定制排序(按年龄升序)
public class TreeSetCustomSortTest {
    public static void main(String[] args) {
        // 传入 Comparator 匿名内部类(Java 8+ 可简化为 Lambda 表达式)
        TreeSet<Employee> set = new TreeSet<>((e1, e2) -> e1.getAge() - e2.getAge());
        set.add(new Employee("张三", 25));
        set.add(new Employee("李四", 22));
        set.add(new Employee("王五", 28));
        for (Employee e : set) {
            System.out.println(e);
        }
        // 输出结果:
        // Employee{name='李四', age=22}
        // Employee{name='张三', age=25}
        // Employee{name='王五', age=28}
    }
}

5.2.3 去重机制(基于排序规则)

TreeSet 的去重机制与 HashSet 不同,它不依赖 hashCode() 和 equals() 方法,而是基于排序规则(compareTo() 或 compare() 方法):

5.3 核心方法源码解析(基于 TreeMap 委托实现)

TreeSet 的核心方法均委托给底层的 TreeMap 实现,TreeMap 的核心是红黑树的插入、删除、查找操作。

add(E e) 方法:元素添加与红黑树插入

public boolean add(E e) {
    // 调用 TreeMap 的 put 方法,key 为元素 e,value 为 PRESENT
    return m.put(e, PRESENT) == null;
}

TreeMap 的 put 方法核心流程:

  1. 若红黑树为空,创建根节点
  2. 若存在比较器,使用比较器的 compare() 方法查找插入位置;否则使用元素的 compareTo() 方法
  3. 若找到相同元素(比较结果为 0),则替换 value,返回旧 value(TreeSet 认为添加失败)
  4. 若未找到相同元素,创建新节点并插入红黑树
  5. 调整红黑树结构(变色、旋转),保证红黑树的平衡特性

红黑树插入调整示例(以插入节点 7 为例):

  1. 插入节点 7 作为红色节点,发现父节点 6 也是红色,违反红黑树特性 4
  2. 进行变色操作:将祖父节点 5 变为红色,父节点 6 和叔父节点 4 变为黑色
  3. 若祖父节点是根节点,再将其变为黑色,调整完成

导航方法(NavigableSet接口)

TreeSet 实现了 NavigableSet 接口,提供了一系列强大的导航方法,用于查找元素的前驱、后继、范围查询等:

// 查找小于 e 的最大元素
E lower(E e);
// 查找小于等于 e 的最大元素
E floor(E e);
// 查找大于等于 e 的最小元素
E ceiling(E e);
// 查找大于 e 的最小元素
E higher(E e);
// 删除并返回最小元素
E pollFirst();
// 删除并返回最大元素
E pollLast();
// 获取升序迭代器
Iterator<E> iterator();
// 获取降序迭代器
Iterator<E> descendingIterator();

示例:导航方法使用

TreeSet<Integer> set = new TreeSet<>();
set.add(10);
set.add(20);
set.add(30);
set.add(40);
System.out.println(set.lower(25)); // 20(小于 25 的最大元素)
System.out.println(set.floor(25)); // 20(小于等于 25 的最大元素)
System.out.println(set.ceiling(25)); // 30(大于等于 25 的最小元素)
System.out.println(set.higher(25)); // 30(大于 25 的最小元素)
System.out.println(set.pollFirst()); // 10(删除最小元素)
System.out.println(set.pollLast()); // 40(删除最大元素)

5.4 TreeSet 性能分析与使用场景

5.4.1 性能特点

操作类型时间复杂度说明
add()/contains()/remove()O(log n)红黑树的平衡特性保证了对数时间复杂度
导航方法(lower/floor/ceiling/higher)O(log n)基于红黑树的二分查找
遍历(iterator())O(n)红黑树的中序遍历,按排序顺序输出

5.4.2 适用场景

5.4.3 注意事项

⚠️ 注意 1:TreeSet 的添加、删除、查找效率低于 HashSet/LinkedHashSet(O(log n) vs O(1)),因此无需排序时,优先选择 HashSet/LinkedHashSet

⚠️ 注意 2:TreeSet 非线程安全,多线程环境下需使用 Collections.synchronizedSortedSet(new TreeSet<>()) 或 JUC 提供的 ConcurrentSkipListSet

⚠️ 注意 3:若元素未实现 Comparable 接口且未指定比较器,添加元素时会抛出 ClassCastException

六、三大Set实现类核心对比与选型指南

6.1 核心特性对比表

特性HashSetLinkedHashSetTreeSet
底层数据结构哈希表(数组+链表/红黑树)哈希表+双向链表红黑树(TreeMap)
元素有序性无序插入顺序自然排序/定制排序
去重机制hashCode() + equals()hashCode() + equals()compareTo()/compare()
时间复杂度(增删查)O(1)(平均)O(1)(平均)O(log n)
内存占用较低较高(额外存储链表指针)中等(红黑树节点 overhead)
线程安全
导航方法支持不支持不支持支持(NavigableSet)
适用场景高效去重,无需有序去重+维护插入顺序去重+排序+范围查询

6.2 选型决策流程图

开始 → 需要元素有序?
├─ 否 → 需要频繁遍历?
│  ├─ 是 → 选择 LinkedHashSet
│  └─ 否 → 选择 HashSet(高效去重)
└─ 是 → 需要排序(自然/定制)?
   ├─ 是 → 需要范围查询/导航?
   │  ├─ 是 → 选择 TreeSet
   │  └─ 否 → 选择 LinkedHashSet(插入顺序)
   └─ 否 → 选择 LinkedHashSet(插入顺序)

6.3 实战选型示例

示例 1:用户注册时存储用户名(需去重,无需有序)

// 选型:HashSet(高效去重,性能最优)
Set<String> usernames = new HashSet<>();
usernames.add("zhangsan");
usernames.add("lisi");
// 重复添加会失败
usernames.add("zhangsan");
System.out.println(usernames.size()); // 1

示例 2:记录用户操作日志(需去重,维护操作顺序)

// 选型:LinkedHashSet(去重+插入顺序)
Set<String> operationLogs = new LinkedHashSet<>();
operationLogs.add("用户登录");
operationLogs.add("查询数据");
operationLogs.add("修改数据");
operationLogs.add("用户退出");
// 遍历顺序与插入顺序一致
for (String log : operationLogs) {
    System.out.println(log); // 用户登录 → 查询数据 → 修改数据 → 用户退出
}

示例 3:学生成绩排行榜(需去重,按分数降序排序,支持TopN查询)

// 选型:TreeSet(去重+排序+导航方法)
TreeSet<Student> scoreRank = new TreeSet<>((s1, s2) -> s2.getScore() - s1.getScore());
scoreRank.add(new Student("001", 90));
scoreRank.add(new Student("002", 95));
scoreRank.add(new Student("003", 88));
// Top3 查询(升序迭代器取前3个)
Iterator<Student> iterator = scoreRank.iterator();
int count = 0;
System.out.println("成绩排行榜 Top3:");
while (iterator.hasNext() && count < 3) {
    System.out.println(++count + ":" + iterator.next());
}

七、实战案例:基于Set的电商商品标签管理系统

7.1 需求分析

设计一个电商商品标签管理系统,支持以下核心功能:

  1. 为商品添加标签(标签不可重复,需维护添加顺序)
  2. 为商品删除指定标签
  3. 查询商品的所有标签(按添加顺序展示)
  4. 查询包含指定标签的所有商品
  5. 统计所有标签的使用频次(按频次降序排序)
  6. 批量导入商品标签,自动去重

7.2 设计思路

7.3 代码实现

7.3.1 商品类(Product.java)

import java.util.LinkedHashSet;
import java.util.Set;
/**
 * 商品类
 */
public class Product {
    private String productId; // 商品ID
    private String productName; // 商品名称
    private Set<String> tags; // 商品标签(LinkedHashSet:去重+插入顺序)
    // 构造函数
    public Product(String productId, String productName) {
        this.productId = productId;
        this.productName = productName;
        this.tags = new LinkedHashSet<>();
    }
    // 添加标签(去重)
    public boolean addTag(String tag) {
        if (tag == null || tag.trim().isEmpty()) {
            throw new IllegalArgumentException("标签不能为空");
        }
        return tags.add(tag.trim());
    }
    // 批量添加标签
    public boolean addTags(Set<String> tags) {
        boolean success = false;
        for (String tag : tags) {
            if (addTag(tag)) {
                success = true;
            }
        }
        return success;
    }
    // 删除标签
    public boolean removeTag(String tag) {
        if (tag == null || tag.trim().isEmpty()) {
            return false;
        }
        return tags.remove(tag.trim());
    }
    // 获取所有标签(返回不可修改集合,防止外部篡改)
    public Set<String> getTags() {
        return new LinkedHashSet<>(tags);
    }
    // getter/setter 省略
    @Override
    public String toString() {
        return "Product{productId='" + productId + "', productName='" + productName + "', tags=" + tags + "}";
    }
}

7.3.2 标签管理类(TagManager.java)

import java.util.*;
import java.util.stream.Collectors;
/**
 * 标签管理类
 */
public class TagManager {
    // 存储商品列表(key:商品ID,value:商品对象)
    private Map<String, Product> productMap = new HashMap<>();
    // 存储标签频次(key:标签,value:使用次数)
    private Map<String, Integer> tagFrequencyMap = new HashMap<>();
    /**
     * 添加商品
     */
    public void addProduct(Product product) {
        if (product == null || product.getProductId() == null) {
            throw new IllegalArgumentException("商品信息不能为空");
        }
        productMap.put(product.getProductId(), product);
        // 初始化商品标签的频次
        for (String tag : product.getTags()) {
            tagFrequencyMap.put(tag, tagFrequencyMap.getOrDefault(tag, 0) + 1);
        }
    }
    /**
     * 为商品添加标签
     */
    public boolean addTagToProduct(String productId, String tag) {
        Product product = productMap.get(productId);
        if (product == null) {
            throw new IllegalArgumentException("商品不存在:" + productId);
        }
        // 商品添加标签(去重)
        boolean success = product.addTag(tag);
        if (success) {
            // 更新标签频次
            tagFrequencyMap.put(tag, tagFrequencyMap.getOrDefault(tag, 0) + 1);
        }
        return success;
    }
    /**
     * 为商品删除标签
     */
    public boolean removeTagFromProduct(String productId, String tag) {
        Product product = productMap.get(productId);
        if (product == null) {
            throw new IllegalArgumentException("商品不存在:" + productId);
        }
        // 商品删除标签
        boolean success = product.removeTag(tag);
        if (success) {
            // 更新标签频次
            int frequency = tagFrequencyMap.getOrDefault(tag, 0);
            if (frequency == 1) {
                tagFrequencyMap.remove(tag);
            } else {
                tagFrequencyMap.put(tag, frequency - 1);
            }
        }
        return success;
    }
    /**
     * 查询包含指定标签的所有商品
     */
    public List<Product> queryProductsByTag(String tag) {
        if (tag == null || tag.trim().isEmpty()) {
            return Collections.emptyList();
        }
        tag = tag.trim();
        // 遍历所有商品,筛选包含该标签的商品
        return productMap.values().stream()
                .filter(product -> product.getTags().contains(tag))
                .collect(Collectors.toList());
    }
    /**
     * 统计标签使用频次(按频次降序排序)
     */
    public Set<Map.Entry<String, Integer>> statTagFrequency() {
        // 使用 TreeSet 按频次降序排序,频次相同按标签字典序升序
        Set<Map.Entry<String, Integer>> sortedSet = new TreeSet<>((e1, e2) -> {
            if (!e1.getValue().equals(e2.getValue())) {
                return e2.getValue() - e1.getValue(); // 频次降序
            }
            return e1.getKey().compareTo(e2.getKey()); // 标签升序
        });
        sortedSet.addAll(tagFrequencyMap.entrySet());
        return sortedSet;
    }
    /**
     * 批量导入商品标签(自动去重)
     */
    public void batchImportTags(String productId, List<String> tags) {
        Product product = productMap.get(productId);
        if (product == null) {
            throw new IllegalArgumentException("商品不存在:" + productId);
        }
        // 批量添加标签(LinkedHashSet 自动去重)
        Set<String> tagSet = new LinkedHashSet<>(tags);
        product.addTags(tagSet);
        // 更新标签频次
        for (String tag : tagSet) {
            tagFrequencyMap.put(tag, tagFrequencyMap.getOrDefault(tag, 0) + 1);
        }
    }
    /**
     * 获取所有商品
     */
    public List<Product> getAllProducts() {
        return new ArrayList<>(productMap.values());
    }
}

7.3.3 测试类(TagManagerTest.java)

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.Map;
/**
 * 标签管理系统测试类
 */
public class TagManagerTest {
    public static void main(String[] args) {
        TagManager tagManager = new TagManager();
        // 1. 创建商品并添加到系统
        System.out.println("=== 1. 创建商品并添加标签 ===");
        Product p1 = new Product("P001", "Java编程思想");
        p1.addTag("编程");
        p1.addTag("Java");
        p1.addTag("书籍");
        p1.addTag("Java"); // 重复标签,自动去重
        tagManager.addProduct(p1);
        Product p2 = new Product("P002", "Python数据分析");
        p2.addTags(new LinkedHashSet<>(Arrays.asList("编程", "Python", "数据分析", "书籍")));
        tagManager.addProduct(p2);
        Product p3 = new Product("P003", "MySQL从入门到精通");
        p3.addTags(new LinkedHashSet<>(Arrays.asList("数据库", "MySQL", "编程", "书籍")));
        tagManager.addProduct(p3);
        // 查看所有商品
        System.out.println("所有商品信息:");
        tagManager.getAllProducts().forEach(System.out::println);
        System.out.println();
        // 2. 为商品添加新标签
        System.out.println("=== 2. 为商品 P001 添加标签 '技术' ===");
        boolean addSuccess = tagManager.addTagToProduct("P001", "技术");
        System.out.println("添加结果:" + (addSuccess ? "成功" : "失败(标签已存在)"));
        System.out.println("P001 最新标签:" + tagManager.getAllProducts().get(0).getTags());
        System.out.println();
        // 3. 为商品删除标签
        System.out.println("=== 3. 为商品 P002 删除标签 '数据分析' ===");
        boolean removeSuccess = tagManager.removeTagFromProduct("P002", "数据分析");
        System.out.println("删除结果:" + (removeSuccess ? "成功" : "失败(标签不存在)"));
        System.out.println("P002 最新标签:" + tagManager.getAllProducts().get(1).getTags());
        System.out.println();
        // 4. 查询包含标签 '编程' 的所有商品
        System.out.println("=== 4. 查询包含标签 '编程' 的商品 ===");
        List<Product> programmingProducts = tagManager.queryProductsByTag("编程");
        programmingProducts.forEach(System.out::println);
        System.out.println();
        // 5. 批量导入商品标签
        System.out.println("=== 5. 为商品 P003 批量导入标签 ===");
        List<String> batchTags = Arrays.asList("技术", "数据库", "后端", "后端"); // 重复标签自动去重
        tagManager.batchImportTags("P003", batchTags);
        System.out.println("P003 批量导入后的标签:" + tagManager.getAllProducts().get(2).getTags());
        System.out.println();
        // 6. 统计标签使用频次
        System.out.println("=== 6. 标签使用频次统计(降序) ===");
        Set<Map.Entry<String, Integer>> tagFrequency = tagManager.statTagFrequency();
        for (Map.Entry<String, Integer> entry : tagFrequency) {
            System.out.printf("标签:%s,使用次数:%d%n", entry.getKey(), entry.getValue());
        }
    }
}

7.4 测试结果与案例总结

7.4.1 测试结果(关键输出)

=== 1. 创建商品并添加标签 ===
所有商品信息:
Product{productId='P001', productName='Java编程思想', tags=[编程, Java, 书籍]}
Product{productId='P002', productName='Python数据分析', tags=[编程, Python, 数据分析, 书籍]}
Product{productId='P003', productName='MySQL从入门到精通', tags=[数据库, MySQL, 编程, 书籍]}

=== 2. 为商品 P001 添加标签 '技术' ===
添加结果:成功
P001 最新标签:[编程, Java, 书籍, 技术]

=== 3. 为商品 P002 删除标签 '数据分析' ===
删除结果:成功
P002 最新标签:[编程, Python, 书籍]

=== 4. 查询包含标签 '编程' 的商品 ===
Product{productId='P001', productName='Java编程思想', tags=[编程, Java, 书籍, 技术]}
Product{productId='P002', productName='Python数据分析', tags=[编程, Python, 书籍]}
Product{productId='P003', productName='MySQL从入门到精通', tags=[数据库, MySQL, 编程, 书籍]}

=== 5. 为商品 P003 批量导入标签 ===
P003 批量导入后的标签:[数据库, MySQL, 编程, 书籍, 技术, 后端]

=== 6. 标签使用频次统计(降序) ===
标签:编程,使用次数:3
标签:书籍,使用次数:3
标签:技术,使用次数:2
标签:数据库,使用次数:2
标签:Java,使用次数:1
标签:MySQL,使用次数:1
标签:Python,使用次数:1
标签:后端,使用次数:1

7.4.2 案例总结

✅ 本案例充分利用了三大 Set 实现类的核心特性:

  1. LinkedHashSet:用于商品标签存储,保证去重和插入顺序,满足标签展示需求
  2. HashSet:批量导入标签时临时存储,快速去重
  3. TreeSet:用于标签频次统计排序,按频次降序展示,满足统计需求

✅ 关键技术亮点:

  1. 数据安全性:getTags() 方法返回标签集合的副本,防止外部直接修改内部数据
  2. 高效去重:通过 LinkedHashSet 和 HashSet 实现不同场景下的快速去重
  3. 灵活排序:使用 TreeSet 自定义比较器,实现标签频次的降序排序
  4. 性能优化:使用 HashMap 维护商品映射和标签频次,保证查询和统计效率

✅ 扩展方向:

八、本章小结

本章深入解析了 Java 集合框架中三大核心 Set 实现类(HashSet、LinkedHashSet、TreeSet)的底层数据结构、核心原理、性能特点及适用场景,通过实战案例展示了 Set 集合在实际开发中的综合应用,同时总结了 Set 集合的选型技巧和常见问题解决方案。

核心要点回顾:

  1. HashSet 基于哈希表实现,核心优势是高效去重(O(1) 平均时间复杂度),适用于无需有序的去重场景
  2. LinkedHashSet 基于哈希表+双向链表实现,兼具去重和插入顺序维护功能,遍历效率高于 HashSet
  3. TreeSet 基于红黑树实现,核心优势是排序和导航功能(O(log n) 时间复杂度),适用于需要排序和范围查询的场景
  4. Set 集合的去重机制:HashSet/LinkedHashSet 依赖 hashCode() + equals(),TreeSet 依赖 compareTo()/compare()
  5. 选型核心原则:根据是否需要有序、是否需要排序、操作频次等因素选择合适的 Set 实现类

通过本章学习,读者应能熟练掌握 Set 集合的使用技巧,根据实际业务场景精准选型,并能解决 Set 集合使用过程中的常见问题(如去重失效、排序异常等)。下一章将深入学习 Java 集合框架中的 Map 接口及其实现类。

以上就是Java集合框架之Set的实现类与实战使用详解的详细内容,更多关于Java Set的实现类的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文