当前位置: 首页 > news >正文

Java 中 Set 接口(更新版)

一、Set 接口的核心特性

1. 元素唯一性

Set 中不允许存在重复元素,这是 Set 接口最显著的特点。其实现原理如下:

判断机制

  • 基于 equals() 和 hashCode() 方法来确定元素是否重复
  • 两个对象如果 equals() 返回 true,则它们的 hashCode() 必须相同
  • 在 HashSet 中,首先比较 hash 值,再调用 equals() 方法确认
  • 在 TreeSet 中,通过 compareTo() 或 Comparator 进行元素比较

添加行为

  • 当尝试添加重复元素时,add() 方法会返回 false
  • 元素不会被存入集合,原有元素保持不变
  • 新元素会覆盖旧元素的情况仅发生在 equals() 返回 true 但属性不同的特殊情况
  • 对于可变对象,修改已存储在 Set 中的元素可能导致意外行为

底层实现

  • HashSet 使用哈希表(数组+链表/红黑树)结构,默认初始容量为 16,负载因子 0.75
  • TreeSet 使用红黑树结构,通过比较器维护元素唯一性和排序
  • LinkedHashSet 在 HashSet 基础上增加双向链表维护插入顺序
  • 并发版本: CopyOnWriteArraySet 适用于读多写少的并发场景

实际应用示例

// 基本类型示例
Set<String> set = new HashSet<>();
System.out.println(set.add("A")); // 输出 true
System.out.println(set.add("A")); // 输出 false
System.out.println(set.size());   // 输出 1// 自定义对象示例
class Person {private String name;private int age;// 必须正确重写equals和hashCode方法@Overridepublic 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);}@Overridepublic int hashCode() {return Objects.hash(name, age);}
}Set<Person> personSet = new HashSet<>();
Person p1 = new Person("张三", 20);
Person p2 = new Person("张三", 20);
personSet.add(p1);
personSet.add(p2); // 不会添加成功,因为equals返回true// 错误示例:未重写equals和hashCode
class BadKey {String id;// 未重写关键方法
}
Set<BadKey> badSet = new HashSet<>();
badSet.add(new BadKey("1"));
badSet.add(new BadKey("1")); // 会被错误地添加为两个元素

2. 无序性

大多数 Set 实现类在元素存储顺序上有以下特点:

基本特性

  • HashSet 不保证元素的存储顺序(基于哈希值的存储)
  • 元素的存入顺序和取出顺序通常不一致
  • 同一程序多次运行,元素的遍历顺序可能不同
  • JDK 8 后,当哈希冲突较多时,链表会转换为红黑树,但顺序仍不可预测

特殊实现类

LinkedHashSet
  • 继承自 HashSet
  • 内部维护一个双向链表记录插入顺序
  • 迭代顺序与插入顺序一致
  • 适合需要保持插入顺序又需要去重的场景
  • 典型应用:实现LRU缓存、记录用户操作序列
TreeSet
  • 基于红黑树实现
  • 元素按照自然顺序或指定的 Comparator 排序
  • 保证元素处于排序状态
  • 所有元素必须实现 Comparable 接口或提供 Comparator
  • 典型应用:排行榜、范围查询、有序数据集

性能对比分析

实现类添加/删除时间复杂度查询时间复杂度内存占用适用场景线程安全
HashSetO(1)O(1)最低普通去重不安全
LinkedHashSetO(1)O(1)中等需要保持插入顺序不安全
TreeSetO(log n)O(log n)最高需要排序不安全
CopyOnWriteArraySetO(n)O(1)读多写少并发场景安全

选择建议

  1. 当不需要关注元素顺序时,使用 HashSet 性能最佳
  2. 需要保持插入顺序时选择 LinkedHashSet(如购物车商品顺序)
  3. 需要排序或范围查询时选择 TreeSet(如按分数排序的学生列表)
  4. 并发环境下考虑 ConcurrentHashMap.newKeySet() 或 CopyOnWriteArraySet

3. 没有索引

Set 接口的访问方式与 List 有以下区别:

访问限制

  • 没有提供 get(int index) 等通过索引访问元素的方法
  • 不能使用传统 for 循环通过索引遍历元素
  • 不能直接修改指定位置的元素
  • 查找特定元素需要遍历或使用 contains() 方法

遍历方式详解

迭代器(Iterator)
  • 最基础的遍历方式
  • 可以在遍历时安全地删除元素
  • 适用于所有 Collection 实现类
  • 示例:
    Iterator<String> it = set.iterator();
    while(it.hasNext()) {String e = it.next();if(e.equals("remove")) {it.remove(); // 安全删除}
    }
    

增强 for 循环(foreach)
  • 语法简洁
  • 编译后实际转换为迭代器实现
  • 遍历时不能修改集合结构(会抛出ConcurrentModificationException)
  • 示例:
    for(String e : set) {System.out.println(e);
    }
    

Java 8 的 forEach() 方法
  • 支持Lambda表达式
  • 内部使用迭代器实现
  • 可读性高,适合简单操作
  • 示例:
    set.forEach(e -> System.out.println(e));
    

Stream API(Java 8+)
  • 支持函数式编程
  • 可以进行过滤、映射等复杂操作
  • 并行处理能力
  • 示例:
    set.stream().filter(s -> s.length() > 3).map(String::toUpperCase).forEach(System.out::println);
    

完整遍历示例

Set<String> set = new HashSet<>(Arrays.asList("Apple", "Banana", "Cherry"));// 方法1:增强for循环(推荐简单遍历)
for(String element : set) {System.out.println(element);
}// 方法2:迭代器(需要删除元素时使用)
Iterator<String> it = set.iterator();
while(it.hasNext()) {String element = it.next();if(element.equals("Banana")) {it.remove(); // 安全删除}
}// 方法3:Java8 forEach
set.forEach(System.out::println);// 方法4:Stream API
set.stream().filter(s -> s.startsWith("A")).map(String::toLowerCase).forEach(System.out::println);// 方法5:并行流处理
set.parallelStream().forEach(System.out::println);

4. 允许存储 null 值

关于 null 值的处理:

不同实现类的处理

HashSet 和 LinkedHashSet
  • 允许存储一个 null 值
  • 尝试添加多个 null 会失败(因为元素唯一性)
  • 查询时需要注意空指针异常
  • 性能考虑:null 的哈希码固定为 0,可能影响哈希分布
TreeSet
  • 不允许存储 null 值
  • 添加 null 会抛出 NullPointerException
  • 原因:排序需要调用 compareTo() 方法
  • 替代方案:使用 Optional 包装或特殊标记值

应用场景分析

数据表示
  • 表示"未知"或"不存在"的值
  • 数据库查询结果映射时表示 NULL 字段
  • 表单提交时表示用户未填写的可选字段
  • 缓存系统中表示"未找到"的状态
特殊处理
Set<String> set = new HashSet<>();
set.add(null); // 允许
set.contains(null); // 需要显式检查// 安全处理示例
if(set.contains(null)) {// 特殊处理逻辑System.out.println("Set contains null value");
}// TreeSet 示例(会抛出异常)
try {TreeSet<String> treeSet = new TreeSet<>();treeSet.add(null); // 抛出 NullPointerException
} catch (NullPointerException e) {System.out.println("TreeSet cannot contain null");
}

注意事项

并发环境
  • Collections.synchronizedSet() 包装后的 Set 对 null 的支持不变
  • 并发修改可能导致不确定行为
  • 建议使用 ConcurrentHashMap.newKeySet() 实现线程安全的Set
序列化
  • 包含 null 的 Set 可以正常序列化/反序列化
  • 自定义序列化时需要特殊处理 null 值
  • JSON序列化库可能有不同的null处理方式
性能影响
  • null 值的哈希码固定为 0
  • 大量 null 值可能影响哈希表性能
  • 考虑使用 Optional.empty() 或特殊常量替代null
框架集成
  • JPA/Hibernate 等ORM框架有特殊处理
  • JSON序列化库可能有不同行为
  • 与其他系统交互时需明确约定 null 的含义
  • REST API 中可能需要特殊处理null值的序列化

最佳实践

  1. 明确区分"不存在"和"空值"的概念
  2. 考虑使用 Optional 包装可能为null的值
  3. 在文档中明确说明Set对null值的处理策略
  4. 在团队内部建立统一的null值处理规范
  5. 对于TreeSet,提前过滤或转换null值

二、Set 接口的常用方法

Set 接口是 Java 集合框架的重要成员,它继承了 Collection 接口的所有方法,同时没有添加新的方法。Set 接口的核心特性是保证元素的唯一性,以下是 Set 接口中常用的方法及其详细说明:

1. boolean add(E e)

作用

向集合中添加元素,确保集合中元素的唯一性

详细说明

  • 返回值

    • 如果元素不存在则添加成功并返回 true
    • 如果元素已存在则返回 false(不添加重复元素)
  • 底层实现机制

    • 在 HashSet 中
      1. 首先计算元素的 hashCode() 确定存储位置
      2. 如果该位置为空,则直接添加
      3. 如果该位置不为空,则通过 equals() 方法比较是否相同
      4. 如果相同则视为重复元素,不添加
    • 在 TreeSet 中
      1. 通过比较器(Comparator)或自然顺序(Comparable)确定元素位置
      2. 如果找到相等的元素(通过 compareTo 返回0)则不添加

示例代码

Set<String> set = new HashSet<>();
boolean added1 = set.add("apple");  // 返回 true
boolean added2 = set.add("apple");  // 返回 false,因为元素已存在
System.out.println(set);  // 输出: [apple]

性能考虑

  • HashSet:平均 O(1) 时间复杂度
  • TreeSet:O(log n) 时间复杂度

应用场景

  • 数据去重处理
  • 创建唯一标识集合
  • 实现数学中的集合运算

2. boolean remove(Object o)

作用

从集合中移除指定元素

详细说明

  • 返回值

    • 如果元素存在则移除成功并返回 true
    • 如果元素不存在则返回 false
  • 注意事项

    1. 对于自定义对象,必须正确重写 equals() 和 hashCode() 方法才能正确移除
    2. 在 TreeSet 中,元素必须实现 Comparable 接口或提供 Comparator
    3. 移除操作可能触发集合内部结构的重新调整

示例代码

Set<String> fruits = new HashSet<>(Arrays.asList("apple", "banana", "orange"));
boolean removed1 = fruits.remove("apple");  // 返回 true
boolean removed2 = fruits.remove("pear");   // 返回 false
System.out.println(fruits);  // 输出: [banana, orange]

性能考虑

  • HashSet:平均 O(1) 时间复杂度
  • TreeSet:O(log n) 时间复杂度

应用场景

  • 从黑名单中移除用户
  • 缓存失效处理
  • 动态更新数据集合

3. boolean contains(Object o)

作用

判断集合中是否包含指定元素

详细说明

  • 返回值

    • 包含则返回 true
    • 不包含则返回 false
  • 时间复杂度

    • HashSet:平均 O(1),最坏情况 O(n)(哈希冲突严重时)
    • TreeSet:O(log n)

示例代码

Set<String> blacklist = new HashSet<>(Arrays.asList("spam", "fraud", "scam"));
if (!blacklist.contains("legit")) {System.out.println("允许访问");
}// 实现不重复添加的常用模式
Set<String> uniqueNames = new HashSet<>();
if (!uniqueNames.contains("John")) {uniqueNames.add("John");
}

应用场景

  • 数据去重时检查元素是否存在
  • 权限验证时检查用户是否拥有某权限
  • 缓存系统中检查键是否存在
  • 实现布隆过滤器等数据结构

4. int size()

作用

返回集合中元素的个数

详细说明

  • 注意事项
    1. 对于超大型集合(超过 Integer.MAX_VALUE 元素),size() 方法可能返回 Integer.MAX_VALUE
    2. 不同于数组的 length 属性,size() 是方法调用
    3. 在并发环境下,size() 的结果可能不是精确的瞬时值

示例代码

Set<Integer> primeNumbers = new HashSet<>(Arrays.asList(2, 3, 5, 7, 11, 13));
int count = primeNumbers.size();
System.out.println("集合包含 " + count + " 个质数");  // 输出: 集合包含 6 个质数// 与空集合比较
if (primeNumbers.size() > 0) {System.out.println("集合不为空");
}

性能考虑

  • 时间复杂度为 O(1)
  • 不需要遍历整个集合

应用场景

  • 统计集合元素数量
  • 检查集合是否为空
  • 资源配额管理

5. boolean isEmpty()

作用

判断集合是否为空(不包含任何元素)

详细说明

  • 实现原理
    • 大多数实现通过检查内部计数器是否为0
    • 比 size() == 0 更高效,因为不需要计算具体数量

示例代码

Set<String> waitingList = new HashSet<>();// 初始化检查
if (waitingList.isEmpty()) {System.out.println("等待列表为空,可以开始处理");
}// 处理完成后检查
waitingList.add("user1");
waitingList.remove("user1");
if (waitingList.isEmpty()) {System.out.println("所有用户已处理完毕");
}

性能考虑

  • 时间复杂度为 O(1)
  • 适合在频繁检查集合是否为空的场景使用

应用场景

  • 初始化检查
  • 处理完成检查
  • 资源状态监控

6. void clear()

作用

清空集合中的所有元素

详细说明

  • 实现细节

    • HashSet:清空内部的哈希表数组,将各位置设为 null
    • TreeSet:清空树结构,将根节点置为 null
    • LinkedHashSet:清空链表和哈希表
  • 内存影响

    • 不会缩小底层存储空间,只是清空元素
    • 已分配的内存保持不变,以备后续添加元素使用
    • 如果需要释放内存,应让集合对象被垃圾回收

示例代码

Set<String> sessionTokens = new HashSet<>();
sessionTokens.add("token1");
sessionTokens.add("token2");System.out.println("当前会话数: " + sessionTokens.size());  // 输出: 2
sessionTokens.clear();
System.out.println("清空后会话数: " + sessionTokens.size());  // 输出: 0

应用场景

  • 会话管理
  • 缓存清理
  • 批量数据处理

7. Iterator<E> iterator()

作用

返回一个迭代器,用于遍历集合中的元素

详细说明

  • 遍历特性

    • HashSet:无序遍历(基于哈希值的顺序,不稳定)
    • LinkedHashSet:按插入顺序遍历
    • TreeSet:按元素自然顺序或比较器顺序遍历
  • fail-fast机制

    • 如果在迭代过程中(除了通过迭代器自身的 remove 方法)修改集合,会抛出 ConcurrentModificationException
    • 这是为了确保遍历时集合状态的一致性

示例代码

Set<String> colors = new TreeSet<>(Arrays.asList("red", "green", "blue"));// 基本遍历
Iterator<String> it = colors.iterator();
while (it.hasNext()) {String color = it.next();System.out.println("颜色: " + color);
}// 使用for-each循环(底层也是使用迭代器)
for (String color : colors) {System.out.println("处理颜色: " + color);
}// 安全删除当前元素
Iterator<String> iterator = colors.iterator();
while (iterator.hasNext()) {String color = iterator.next();if (color.equals("green")) {iterator.remove();  // 安全删除方式}
}

应用场景

  • 集合元素遍历
  • 条件性删除元素
  • 批量数据处理

选择适当的 Set 实现

这些方法构成了 Set 接口的核心功能,使得 Set 能够高效地管理不重复元素的集合。在实际开发中,根据具体需求可以选择不同的 Set 实现:

HashSet

  • 特点
    • 需要快速查找时使用
    • 不关心元素顺序
    • 基于哈希表实现,性能最佳
  • 示例应用
    • 用户黑名单
    • 单词计数器
    • 唯一ID集合

TreeSet

  • 特点
    • 需要元素排序时使用
    • 元素必须实现 Comparable 或提供 Comparator
    • 基于红黑树实现,保持有序
  • 示例应用
    • 排行榜
    • 按日期排序的事件
    • 范围查询

LinkedHashSet

  • 特点
    • 需要保持插入顺序时使用
    • 内部维护插入顺序的链表
    • 性能略低于 HashSet
  • 示例应用
    • 网页访问历史记录
    • 操作日志
    • LRU缓存实现

重要注意事项

  1. 唯一性保证

    • Set 不允许重复元素的特点,add() 方法的行为与 List 接口有所不同
    • 重复的判断基于 equals() 和 hashCode() 方法
  2. 方法实现要求

    • Set 实现通常依赖元素的 equals() 和 hashCode() 方法
    • 对于自定义对象必须正确实现这两个方法
  3. 并发考虑

    • 在并发环境下应考虑使用 ConcurrentSkipListSet 或 Collections.synchronizedSet()
    • 标准 Set 实现不是线程安全的
  4. 性能权衡

    • 根据需求在查找速度、顺序保持和排序功能之间进行选择
    • 大型集合要考虑内存使用效率
  5. 特殊场景

    • 对于枚举类型,考虑使用 EnumSet 获得最佳性能
    • 对于只读集合,可以使用 Collections.unmodifiableSet()

三、Set 接口的常用实现类

Set 接口是 Java 集合框架中的重要组成部分,它定义了不允许包含重复元素的集合。Set 接口有三个主要的实现类:HashSet、LinkedHashSet 和 TreeSet。它们在底层实现、性能和功能上各有特点,适用于不同的应用场景。

1. HashSet

基本概念

HashSet 是 Java 集合框架中 Set 接口最常用的实现类,其底层是通过 HashMap 来实现的。它实现了 Set 接口的所有方法,并提供了额外的功能。

详细特性

存储结构

HashSet 的底层实现基于哈希表,具体结构为:

  • 数组 + 链表/红黑树的组合结构
  • 当链表长度超过阈值(默认为8)且数组长度大于等于64时,链表会转换为红黑树
  • 当红黑树节点数小于等于6时,会退化为链表
  • 元素的存储位置由元素的 hashCode() 方法决定,通过哈希算法计算索引位置

元素特性

  • 无序性:元素存储顺序与添加顺序无关,也不保证任何特定顺序
  • 不可预测性:存储顺序可能会随着元素的添加、删除和扩容发生变化
  • 唯一性:通过 equals() 和 hashCode() 方法保证元素唯一性,不允许重复元素
  • 线程不安全:不是线程安全的集合

性能特点

  • 时间复杂度

    • 添加(add):平均O(1),最坏O(n)
    • 删除(remove):平均O(1),最坏O(n)
    • 查找(contains):平均O(1),最坏O(n)
  • 容量相关参数

    • 默认初始容量为16
    • 默认负载因子为0.75(当元素数量达到容量*负载因子时扩容)
    • 扩容时容量变为原来的2倍

特殊值处理

  • 允许存储一个 null 值
  • 存储 null 时会被放在哈希表的第一个位置(索引0处)

使用场景

  1. 去重应用

    • 从数据流中去除重复元素
    • 统计不重复的IP地址
    • 过滤重复的用户ID
  2. 快速查找

    • 黑名单/白名单检查
    • 缓存系统
    • 需要频繁判断元素是否存在的情况
  3. 集合运算

    • 求两个集合的并集、交集、差集
    • 数据集对比分析

完整示例代码

import java.util.HashSet;
import java.util.Set;public class HashSetExample {public static void main(String[] args) {// 创建HashSetSet<String> fruits = new HashSet<>();// 添加元素fruits.add("apple");fruits.add("banana");fruits.add("orange");boolean isAdded = fruits.add("apple"); // 尝试添加重复元素System.out.println("Was apple added again? " + isAdded); // 输出false// 检查元素是否存在System.out.println("\nContains banana: " + fruits.contains("banana"));System.out.println("Contains peach: " + fruits.contains("peach"));// 遍历集合(顺序不确定)System.out.println("\nAll fruits (initial order):");for (String fruit : fruits) {System.out.println(fruit);}// 移除元素boolean isRemoved = fruits.remove("orange");System.out.println("\nWas orange removed? " + isRemoved);System.out.println("Current size: " + fruits.size());// 添加null值fruits.add(null);System.out.println("\nAfter adding null:");for (String fruit : fruits) {System.out.println(fruit);}// 清空集合fruits.clear();System.out.println("\nAfter clearing, size: " + fruits.size());System.out.println("Is empty? " + fruits.isEmpty());// 批量操作示例Set<String> tropicalFruits = new HashSet<>();tropicalFruits.add("mango");tropicalFruits.add("pineapple");tropicalFruits.add("banana");fruits.addAll(tropicalFruits);System.out.println("\nAfter addAll:");System.out.println(fruits);}
}

注意事项

  1. 对象相等性

    • 存储在HashSet中的对象必须正确实现equals()和hashCode()方法
    • 两个对象equals()为true,则它们的hashCode()必须相同
  2. 性能调优

    • 如果预先知道元素数量,可以设置初始容量以避免多次扩容
    • 对于特别大的集合,可以适当调整负载因子
  3. 线程安全

    • 多线程环境下需要使用Collections.synchronizedSet()包装
    • 或者考虑使用ConcurrentHashMap.newKeySet()
  4. 迭代顺序

    • 如果需要有序迭代,可以考虑LinkedHashSet
    • 如果需要排序,可以考虑TreeSet

2. LinkedHashSet

继承关系与实现原理

LinkedHashSet 是 Java 集合框架中 HashSet 的一个特殊子类,它通过继承 HashSet 并实现 Set 接口来提供有序的集合功能。其底层实现结合了两种数据结构:

  1. 哈希表结构(继承自 HashSet):

    • 使用数组+链表/红黑树(JDK8+)的存储方式
    • 通过元素的 hashCode() 值确定存储位置
    • 保证元素的唯一性(不允许重复)
  2. 双向链表结构(新增特性):

    • 维护元素之间的前驱和后继引用
    • 按插入顺序记录所有元素
    • 在迭代时按链表顺序遍历

核心特性对比

特性HashSetLinkedHashSetTreeSet
有序性插入顺序自然/自定义排序
时间复杂度O(1)O(1)O(log n)
允许null否(除非指定Comparator)
实现原理哈希表哈希表+链表红黑树

性能深度分析

时间复杂度详解

  • 添加操作:平均 O(1),需要同时更新哈希表和链表
  • 删除操作:平均 O(1),需要解除链表节点的前后引用
  • 查找操作:平均 O(1),与 HashSet 相同
  • 迭代操作:O(n),直接遍历链表比 HashSet 的数组遍历更高效

内存占用

相比 HashSet 额外存储:

  • 每个元素的 Entry 对象包含 before 和 after 引用
  • 维护头尾指针的额外空间开销
  • 通常比 HashSet 多占用 20%-30% 内存

高级应用场景

1. LRU 缓存实现

public class LRUCache<K,V> extends LinkedHashMap<K,V> {private final int capacity;public LRUCache(int capacity) {super(capacity, 0.75f, true);this.capacity = capacity;}@Overrideprotected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return size() > capacity;}
}

2. 有序去重处理

// 保持原始数据顺序的同时去重
List<Integer> numbers = Arrays.asList(3,1,4,1,5,9,2,6,5,3);
Set<Integer> uniqueNumbers = new LinkedHashSet<>(numbers);
// 输出:[3, 1, 4, 5, 9, 2, 6]

3. 访问顺序跟踪

// 记录用户访问页面的顺序
LinkedHashSet<String> visitedPages = new LinkedHashSet<>();
visitedPages.add("首页");
visitedPages.add("产品页");
visitedPages.add("购物车");
// 可以准确获取用户浏览路径

最佳实践建议

  1. 初始化容量设置

    // 预估元素数量,避免扩容
    LinkedHashSet<String> set = new LinkedHashSet<>(100);
    

  2. 迭代性能优化

    // 使用 iterator() 比增强for循环稍快
    Iterator<String> it = set.iterator();
    while(it.hasNext()) {String item = it.next();// 处理逻辑
    }
    

  3. 与ArrayList转换

    // 保持顺序转换为List
    List<String> list = new ArrayList<>(linkedHashSet);
    

  4. 并发环境替代方案

    Set<String> safeSet = Collections.synchronizedSet(new LinkedHashSet<>());
    // 或使用 ConcurrentHashMap 实现的并发Set
    

注意事项

  1. 对象可变性:如果存储在 LinkedHashSet 中的对象修改了影响 hashCode() 的字段,会导致定位失败

    Set<Person> people = new LinkedHashSet<>();
    Person p = new Person("Alice");
    people.add(p);
    p.setName("Bob");  // 危险操作!
    

  2. 顺序维护:以下操作会改变元素顺序:

    • 先 remove() 再 add() 同一元素
    • 使用 clone() 方法复制集合
    • 通过序列化/反序列化恢复集合
  3. 性能监控:当元素数量极大时(>10^6),链表维护可能成为瓶颈,此时应考虑:

    • 使用普通 HashSet 放弃顺序
    • 采用专门的有序集合库
    • 实现自定义的分片存储方案

3. TreeSet

基本概念

TreeSet 是基于 TreeMap 实现的 NavigableSet 实现类,它存储的元素按照一定的顺序排列。与 HashSet 不同,TreeSet 保证了元素的有序性,这种有序性是通过红黑树(Red-Black tree)数据结构实现的。

详细特性

存储结构

TreeSet 内部使用红黑树(一种自平衡二叉搜索树)来存储元素:

  • 每个节点最多有两个子节点
  • 左子节点的值小于父节点
  • 右子节点的值大于父节点
  • 通过自平衡机制保证树的高度相对平衡

元素排序方式:

  • 自然顺序(元素实现 Comparable 接口)
  • 或者通过构造时传入的 Comparator 指定排序规则

元素特性

  1. 自动排序:元素插入时会自动按照排序规则找到合适位置
  2. 元素唯一性保证机制:
    • 通过 compareTo() 方法(自然排序)
    • 或者 compare() 方法(自定义比较器)
    • 当比较结果为 0 时视为相同元素,新元素不会被添加
  3. 元素比较:实际比较的是元素的内容而非内存地址

性能特点

  • 添加、删除和查找操作的时间复杂度均为 O(logN)
  • 相比 HashSet(O(1))和 LinkedHashSet(O(1))性能较慢
  • 适合需要排序的场景,不适合单纯需要快速查找的场景

特殊值处理

  • 不允许 null 值:尝试添加 null 会抛出 NullPointerException
  • 原因:null 无法与其他元素进行有意义的比较

使用场景

适合场景

  1. 需要元素自动排序的集合
    • 如成绩排名、时间排序等
  2. 需要范围查询的操作
    • 如查找某个分数段的学生
  3. 需要获取子集、头部集或尾部集
    • 如获取前10名或后10名的数据

不适合场景

  1. 需要频繁插入删除且不关心顺序
  2. 需要存储 null 值的集合
  3. 对性能要求极高的纯查找场景

完整示例代码

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;public class TreeSetExample {public static void main(String[] args) {// 自然排序示例 - 整数默认按升序排列Set<Integer> numbers = new TreeSet<>();numbers.add(5);numbers.add(2);numbers.add(8);numbers.add(1);numbers.add(5); // 重复元素不会被添加System.out.println("Numbers in natural order:");for (Integer num : numbers) {System.out.println(num); // 输出:1, 2, 5, 8}// 自定义排序示例 - 按字符串长度降序排列Comparator<String> lengthComparator = (s1, s2) -> {// 先按长度降序int lengthCompare = Integer.compare(s2.length(), s1.length());// 长度相同则按字母顺序return lengthCompare != 0 ? lengthCompare : s1.compareTo(s2);};Set<String> words = new TreeSet<>(lengthComparator);words.add("apple");words.add("banana");words.add("orange");words.add("pear");words.add("grape");words.add("kiwi");System.out.println("\nWords sorted by length (descending):");for (String word : words) {System.out.println(word); // 输出:banana, orange, apple, grape, pear, kiwi}// 特殊操作示例 - 导航方法TreeSet<Integer> scores = new TreeSet<>();scores.add(85);scores.add(92);scores.add(78);scores.add(95);scores.add(88);System.out.println("\nScores operations:");System.out.println("First (lowest): " + scores.first()); // 78System.out.println("Last (highest): " + scores.last());  // 95System.out.println("HeadSet(90): " + scores.headSet(90)); // [78, 85, 88]System.out.println("TailSet(90): " + scores.tailSet(90)); // [92, 95]System.out.println("SubSet(80, 90): " + scores.subSet(80, 90)); // [85, 88]// 尝试添加null值try {words.add(null); // 抛出NullPointerException} catch (NullPointerException e) {System.out.println("\nCannot add null to TreeSet - " + e.getMessage());}// 实际应用示例:学生成绩排名TreeSet<Student> students = new TreeSet<>(Comparator.comparing(Student::getScore).reversed().thenComparing(Student::getName));students.add(new Student("Alice", 88));students.add(new Student("Bob", 92));students.add(new Student("Charlie", 85));students.add(new Student("David", 92)); // 同分不同名students.add(new Student("Eve", 85));System.out.println("\nStudent ranking:");for (Student s : students) {System.out.println(s.getName() + ": " + s.getScore());}}static class Student {private String name;private int score;public Student(String name, int score) {this.name = name;this.score = score;}public String getName() { return name; }public int getScore() { return score; }}
}

注意事项

  1. 线程不安全:多线程环境下需要外部同步
  2. 快速失败迭代器:迭代过程中修改集合会抛出 ConcurrentModificationException
  3. 元素可变性问题:如果排序依赖的元素属性被修改,可能导致集合行为异常

总结比较

特性HashSetLinkedHashSetTreeSet
底层实现哈希表哈希表+链表红黑树
元素顺序无序插入顺序排序顺序
性能O(1)O(1)O(logN)
允许null
线程安全
实现接口SetSetNavigableSet
典型应用场景快速查找保持插入顺序需要排序

开发者应根据具体需求选择合适的Set实现类,在不需要排序时优先考虑HashSet,需要保持插入顺序时使用LinkedHashSet,需要排序功能时选择TreeSet。

四、Set 接口使用的注意事项

equals() 和 hashCode() 方法的重写

在使用 HashSet 和 LinkedHashSet 时,如果存储的是自定义对象,必须重写对象的 equals() 方法和 hashCode() 方法,以保证元素的唯一性。重写时需要遵循以下重要规则:

  1. 反射性:任何非空引用值 x,x.equals(x) 应返回 true
  2. 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true
  3. 传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true
  4. 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回相同结果
  5. 非空性:对于任何非空引用值 x,x.equals(null) 应返回 false

equals() 与 hashCode() 的一致性

最重要的规则是:如果两个对象通过 equals() 方法比较相等,则它们的 hashCode() 方法必须返回相同的值。但反过来不成立 - 两个对象有相同的 hashCode 值,它们不一定 equals。

示例实现(对于 Student 类):

public class Student {private String studentId;private String name;@Overridepublic boolean equals(Object o) {// 1. 检查是否是同一个对象if (this == o) return true;// 2. 检查对象是否为null或者是不同类if (o == null || getClass() != o.getClass()) return false;// 3. 类型转换Student student = (Student) o;// 4. 比较关键字段return studentId.equals(student.studentId);}@Overridepublic int hashCode() {// 使用Objects.hash()方法,确保hashCode只基于equals比较的字段return Objects.hash(studentId);}
}

哈希冲突处理

哈希冲突是指两个不同的对象具有相同的哈希码值。这是完全允许的情况,但需要注意:

  1. 好的哈希函数应尽量减少冲突
  2. 当冲突发生时,集合会使用equals()方法进行进一步比较
  3. 冲突会影响性能但不会影响正确性

优化哈希函数的技巧:

  • 使用质数作为乘数
  • 包含所有重要字段
  • 避免使用可变字段

实现类选择指南

需求场景推荐实现类时间复杂度特点
快速查找/插入/删除,不关心顺序HashSetO(1)基于哈希表实现,性能最好
保持插入顺序LinkedHashSetO(1)内部维护一个双向链表记录插入顺序
需要自然或自定义排序TreeSetO(log n)基于红黑树实现,自动排序,但插入和删除操作比HashSet慢

典型应用场景

  1. 用户ID集合(唯一性):使用HashSet存储所有注册用户的ID,确保每个用户ID唯一

    Set<String> userIds = new HashSet<>();
    

  2. 最近访问记录(保持顺序):使用LinkedHashSet记录用户最近访问的10个页面

    Set<String> recentPages = new LinkedHashSet<>(10);
    

  3. 成绩排名(需要排序):使用TreeSet存储学生成绩并自动排序

    Set<Student> rankedStudents = new TreeSet<>(Comparator.comparing(Student::getScore).reversed());
    

元素的不可变性问题

问题:如果Set集合中存储的是可变对象,修改其属性可能导致:

  • hashCode值变化,导致无法正常查找
  • 比较结果变化,破坏TreeSet的有序性

解决方案

  1. 优先使用不可变对象(如String、Integer等)
  2. 如果必须使用可变对象:
    • 确保关键属性不会改变(设为final)
    • 修改对象后从集合中移除再重新添加
    • 使用防御性拷贝

示例:

Set<MutableObject> set = new HashSet<>();
MutableObject obj = new MutableObject("initial");
set.add(obj);// 错误做法:直接修改
obj.setValue("changed"); // 可能导致对象在集合中"丢失"// 正确做法:先移除再修改再添加
set.remove(obj);
obj.setValue("changed");
set.add(obj);

遍历方式

Set集合没有索引,常用遍历方式包括:

  1. 增强for循环(最简单):

    for (String item : set) {System.out.println(item);
    }
    

  2. 迭代器(Iterator)

    Iterator<String> it = set.iterator();
    while (it.hasNext()) {System.out.println(it.next());// 可以在遍历时安全删除元素it.remove(); 
    }
    

  3. Java 8+ Stream API

    set.stream().filter(item -> item.length() > 3).forEach(System.out::println);
    

  4. 并行流(大数据量时):

    set.parallelStream().forEach(System.out::println);
    

线程安全性

问题:Set接口的所有实现类(HashSet、LinkedHashSet、TreeSet)都是非线程安全的。

解决方案

  1. 使用Collections.synchronizedSet()包装:

    Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
    // 使用时需要同步块
    synchronized(syncSet) {if (!syncSet.contains("key")) {syncSet.add("key");}
    }
    

  2. 使用并发集合(如ConcurrentHashMap的KeySet):

    Set<String> concurrentSet = ConcurrentHashMap.newKeySet();
    // 线程安全操作
    concurrentSet.add("item");
    

  3. 使用CopyOnWriteArraySet(适合读多写少场景):

    Set<String> copyOnWriteSet = new CopyOnWriteArraySet<>();
    

注意事项

  • 即使使用同步集合,复合操作(如检查再添加)仍需要额外同步
  • 考虑使用java.util.concurrent包中的并发集合替代同步包装
  • 同步集合会降低性能,只在必要时使用

TreeSet的排序问题

自然排序(元素实现Comparable接口)

  1. 元素类必须实现Comparable<T>接口
  2. 重写compareTo()方法
  3. 示例实现:
public class Student implements Comparable<Student> {private String studentId;private String name;@Overridepublic int compareTo(Student s) {// 按学号升序排列return this.studentId.compareTo(s.studentId);}// 注意:必须保持compareTo与equals一致@Overridepublic boolean equals(Object o) {if (this == o) return true;if (!(o instanceof Student)) return false;Student student = (Student) o;return studentId.equals(student.studentId);}@Overridepublic int hashCode() {return Objects.hash(studentId);}
}

自定义排序(通过Comparator接口)

  1. 创建TreeSet时指定Comparator
  2. 示例:
// 按年龄升序排列
TreeSet<Student> studentsByAge = new TreeSet<>(Comparator.comparingInt(Student::getAge)
);// 按姓名降序排列
TreeSet<Student> studentsByName = new TreeSet<>((s1, s2) -> s2.getName().compareTo(s1.getName())
);// 多条件排序:先按年龄,年龄相同按姓名
TreeSet<Student> students = new TreeSet<>(Comparator.comparingInt(Student::getAge).thenComparing(Student::getName)
);

唯一性规则

无论是自然排序还是自定义排序,比较结果为0的两个元素会被视为相同元素,不会被同时存入集合。这意味着:

  1. 比较逻辑决定了什么是"相同"元素
  2. 如果希望保留比较相等但实际不同的对象,需要调整比较逻辑
  3. 示例问题:
TreeSet<Student> set = new TreeSet<>(Comparator.comparingInt(Student::getAge));
set.add(new Student("1", "Alice", 20));
set.add(new Student("2", "Bob", 20)); // 不会被添加,因为年龄相同

解决方案是细化比较条件:

TreeSet<Student> set = new TreeSet<>(Comparator.comparingInt(Student::getAge).thenComparing(Student::getId)
);

http://www.dtcms.com/a/349181.html

相关文章:

  • 深度学习中的“集体智慧”:Dropout技术详解——不仅是防止过拟合,更是模型集成的革命
  • Java静态代理与动态代理实战解析
  • redis集群模式 -批量操作
  • 智慧工业设备巡检误报率↓81%!陌讯多模态融合算法实战优化与边缘部署
  • 【机器学习】6 Frequentist statistics
  • (计算机网络)JWT三部分及 Signature 作用
  • 车企数据资产管理——解读46页大型车企数据资产数据治理体系解决方案【附全文阅读】
  • 计算机系统 C语言运行时对应内存映射 以及 各个段的数据访问下标越界产生的后果
  • Delphi 12 基于 Indy 的 WebServer 的 https 实现的问题
  • HiRAG:用分层知识图解决复杂推理问题
  • ruoyi框架角色分配用户
  • imx6ull-驱动开发篇38——Linux INPUT 子系统
  • leetcode_189 轮转数组
  • 什么嵌入式接入大模型:第二篇基于 STM32 ESP32 的社会服务助手
  • AI重塑跨境电商:选品成功率提升53%+物流效率加快34%,多语种运营成破局关键
  • String的intern方法
  • 数据库服务优化设置
  • nano命令使用方法
  • 备考NCRE三级信息安全技术 --- L1 信息安全保障概述
  • 自编 C# 颜色命名和色彩显示,使用 DataGridView 展示颜色命名、RGB值
  • 推进数据成熟度旅程的 3 个步骤
  • 基于 MATLAB 的信号处理实战:滤波、傅里叶变换与频谱分析
  • 什么是IP代理
  • 智慧农业病虫害监测误报率↓82%!陌讯多模态融合算法实战解析
  • 基于微信小程序校园微店源码
  • 电力电子simulink练习10:反激Flyback电路搭建
  • [leetcode] - 不定长滑动窗口
  • 深度学习卷积神经网络项目实战
  • 电容触控:自电容 vs 互电容
  • Rust 登堂 生命周期(一)