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

Java 中 Set 接口详解:知识点与注意事项

一、Set 接口的核心特性

Set 接口作为 Collection 的子接口,具有以下核心特性:

1. 元素唯一性

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

  • 判断元素是否重复是基于 equals() 和 hashCode() 方法
  • 当尝试添加重复元素时,add() 方法会返回 false,且元素不会被存入集合
  • 底层通过哈希表(HashSet)或红黑树(TreeSet)等数据结构来确保唯一性

实际应用示例:

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

2. 无序性

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

  • HashSet 不保证元素的存储顺序(基于哈希值的存储)
  • 元素的存入顺序和取出顺序可能不一致
  • 例外情况:
    • LinkedHashSet 会维护元素的插入顺序(双向链表实现)
    • TreeSet 会根据比较器(Comparator)或自然顺序维护排序顺序

性能对比:

  • 当不需要关注元素顺序时,使用 HashSet 性能最佳(O(1)时间复杂度)
  • 需要保持插入顺序时选择 LinkedHashSet(稍慢于 HashSet)
  • 需要排序时选择 TreeSet(O(log n)时间复杂度)

3. 没有索引

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

  • 没有提供 get(int index) 等通过索引访问元素的方法
  • 不能使用传统 for 循环通过索引遍历元素
  • 遍历方式:
    • 迭代器(Iterator)
    • 增强 for 循环(foreach)
    • Java 8 的 forEach() 方法

遍历示例:

// 方法1:增强for循环
for(String element : set) {System.out.println(element);
}// 方法2:迭代器
Iterator<String> it = set.iterator();
while(it.hasNext()) {System.out.println(it.next());
}// 方法3:Java8 forEach
set.forEach(System.out::println);

4. 允许存储 null 值

关于 null 值的处理:

  • HashSet 和 LinkedHashSet:
    • 允许存储一个 null 值
    • 不能存储多个 null 值(因为元素具有唯一性)
  • TreeSet:
    • 不允许存储 null 值(需要调用 compareTo() 方法)
    • 会抛出 NullPointerException

应用场景:

  • 当需要表示"未知"或"不存在"的值时可以使用 null
  • 数据库查询结果映射时,可用 null 表示字段值为 NULL
  • 在数据校验时,可用 null 表示缺失的必填字段

注意事项:

  • 操作包含 null 的 Set 时要注意空指针异常
  • 在并发环境下,Collections.synchronizedSet() 包装后的 Set 对 null 的支持与原始实现一致

二、Set 接口的常用方法

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

1. boolean add(E e)

作用:向集合中添加元素

返回值

  • 如果元素不存在则添加成功并返回 true
  • 如果元素已存在则返回 false(不添加重复元素)

底层实现机制

  • HashSet 中:
    • 首先计算元素的 hashCode() 确定存储位置
    • 如果该位置为空,则直接添加
    • 如果该位置不为空,则通过 equals() 方法比较是否相同
    • 如果相同则视为重复元素,不添加
  • TreeSet 中:
    • 通过比较器(Comparator)或自然顺序(Comparable)确定元素位置
    • 如果找到相等的元素(通过 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]

3. boolean contains(Object o)

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

返回值

  • 包含则返回 true
  • 不包含则返回 false

时间复杂度

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

应用场景

  1. 数据去重时检查元素是否存在
  2. 权限验证时检查用户是否拥有某权限
  3. 缓存系统中检查键是否存在

示例

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("集合不为空");
}

5. boolean isEmpty()

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

实现原理

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

性能考虑

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

示例

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

6. void clear()

作用:清空集合中的所有元素

实现细节

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

内存影响

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

示例

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 实现:

  1. HashSet

    • 需要快速查找时使用
    • 不关心元素顺序
    • 基于哈希表实现,性能最佳
    • 示例:用户黑名单、单词计数器
  2. TreeSet

    • 需要元素排序时使用
    • 元素必须实现 Comparable 或提供 Comparator
    • 基于红黑树实现,保持有序
    • 示例:排行榜、按日期排序的事件
  3. LinkedHashSet

    • 需要保持插入顺序时使用
    • 内部维护插入顺序的链表
    • 性能略低于 HashSet
    • 示例:网页访问历史记录、操作日志

重要注意事项

  1. Set 不允许重复元素的特点,add() 方法的行为与 List 接口有所不同
  2. Set 实现通常依赖元素的 equals()hashCode() 方法
  3. 对于自定义对象必须正确实现这两个方法
  4. 在并发环境下应考虑使用 ConcurrentSkipListSetCollections.synchronizedSet()

三、Set 接口的常用实现类

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

1. HashSet

HashSet 是 Set 接口最常用的实现类,其底层是通过 HashMap 来实现的。它具有以下特点:

详细特性

  1. 存储结构

    • 基于哈希表实现(数组 + 链表/红黑树)
    • 当链表长度超过阈值(默认为8)时,链表会转换为红黑树
    • 元素的存储位置由元素的 hashCode() 方法决定
  2. 元素特性

    • 无序存储,不保证元素的迭代顺序
    • 存储顺序可能会随着元素的添加、删除和扩容发生变化
    • 通过 equals() 和 hashCode() 方法保证元素唯一性
  3. 性能特点

    • 添加、删除和查找操作的平均时间复杂度为 O(1)
    • 性能受初始容量和负载因子影响
    • 默认初始容量为16,负载因子为0.75
  4. 特殊值处理

    • 允许存储一个 null 值

使用场景

  • 需要快速查找元素的集合
  • 不关心元素存储顺序的应用
  • 需要去重的数据集合

完整示例代码

import java.util.HashSet;
import java.util.Set;public class HashSetExample {public static void main(String[] args) {Set<String> fruits = new HashSet<>();// 添加元素fruits.add("apple");fruits.add("banana");fruits.add("orange");fruits.add("apple"); // 重复元素不会添加// 检查元素是否存在System.out.println("Contains banana: " + fruits.contains("banana"));// 遍历集合(顺序不确定)System.out.println("\nAll fruits:");for (String fruit : fruits) {System.out.println(fruit);}// 移除元素fruits.remove("orange");System.out.println("\nAfter removing orange, 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());}
}

2. LinkedHashSet

LinkedHashSet 是 HashSet 的子类,它在 HashSet 的基础上,通过链表维护了元素的存储顺序。

详细特性

  1. 存储结构

    • 继承自 HashSet
    • 内部使用哈希表+双向链表结构
    • 哈希表保证元素唯一性
    • 双向链表维护插入顺序
  2. 元素特性

    • 保证元素的插入顺序(FIFO)
    • 迭代顺序与添加顺序一致
    • 同样通过 equals() 和 hashCode() 保证唯一性
  3. 性能特点

    • 性能略低于 HashSet(需要维护链表)
    • 添加、删除和查找操作的平均时间复杂度仍为 O(1)
    • 比TreeSet性能更好
  4. 特殊值处理

    • 允许存储一个 null 值

使用场景

  • 需要维护插入顺序的集合
  • 需要快速查找且保持顺序的场景
  • LRU缓存实现的基础数据结构

完整示例代码

import java.util.LinkedHashSet;
import java.util.Set;public class LinkedHashSetExample {public static void main(String[] args) {Set<String> cities = new LinkedHashSet<>();// 添加元素cities.add("Beijing");cities.add("Shanghai");cities.add("Guangzhou");cities.add("Shenzhen");cities.add("Beijing"); // 重复元素// 遍历集合(保持插入顺序)System.out.println("Cities in insertion order:");for (String city : cities) {System.out.println(city);}// 访问元素不会改变顺序System.out.println("\nContains Shanghai: " + cities.contains("Shanghai"));// 移除并重新添加元素会改变顺序cities.remove("Guangzhou");cities.add("Guangzhou");System.out.println("\nAfter modification:");for (String city : cities) {System.out.println(city);}// 性能测试long startTime = System.nanoTime();for (int i = 0; i < 10000; i++) {cities.add("City" + i);}long endTime = System.nanoTime();System.out.println("\nTime to add 10000 elements: " + (endTime - startTime) + " ns");}
}

3. TreeSet

TreeSet 是基于 TreeMap 实现的 NavigableSet 实现类,元素按照一定的顺序排列。

详细特性

  1. 存储结构

    • 基于红黑树(自平衡二叉搜索树)
    • 元素按照自然顺序或指定比较器排序
  2. 元素特性

    • 元素自动排序
    • 通过比较器或 Comparable 接口保证元素唯一性
    • compareTo() 或 compare() 返回0视为相同元素
  3. 性能特点

    • 添加、删除和查找操作的时间复杂度为 O(logN)
    • 比HashSet和LinkedHashSet慢
    • 适合需要排序的场景
  4. 特殊值处理

    • 不允许null值(会抛出NullPointerException)

使用场景

  • 需要元素自动排序的集合
  • 需要范围查询的操作
  • 需要获取子集、头部集或尾部集

完整示例代码

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);}// 自定义排序示例Comparator<String> lengthComparator = (s1, s2) -> s2.length() - s1.length();Set<String> words = new TreeSet<>(lengthComparator);words.add("apple");words.add("banana");words.add("orange");words.add("pear");words.add("grape");System.out.println("\nWords sorted by length (descending):");for (String word : words) {System.out.println(word);}// 特殊操作示例TreeSet<Integer> scores = new TreeSet<>();scores.add(85);scores.add(92);scores.add(78);scores.add(95);scores.add(88);System.out.println("\nScores:");System.out.println("First: " + scores.first());System.out.println("Last: " + scores.last());System.out.println("HeadSet(90): " + scores.headSet(90));System.out.println("TailSet(90): " + scores.tailSet(90));// 尝试添加null值try {words.add(null); // 抛出NullPointerException} catch (NullPointerException e) {System.out.println("\nCannot add null to TreeSet");}}
}

总结比较

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

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

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

重写 equals() 和 hashCode() 方法

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

  1. equals() 与 hashCode() 的一致性:如果两个对象通过 equals() 方法比较相等,则它们的 hashCode() 方法必须返回相同的值。

    • 示例:对于 Student 类,如果认为两个学生的学号相同即为同一个学生,则 equals() 方法应比较学号,hashCode() 也应基于学号生成
    • 实现模板:
      @Override
      public boolean equals(Object o) {if (this == o) return true;if (!(o instanceof Student)) return false;Student student = (Student) o;return studentId.equals(student.studentId);
      }@Override
      public int hashCode() {return Objects.hash(studentId);
      }
      

  2. 哈希冲突处理:如果两个对象的 hashCode() 方法返回相同的值,则它们通过 equals() 方法比较不一定相等(哈希冲突是允许的)。

实现类选择指南

需求场景推荐实现类时间复杂度特点
快速查找/插入/删除,不关心顺序HashSetO(1)基于哈希表,性能最好
保持插入顺序LinkedHashSetO(1)维护插入顺序的链表
需要排序TreeSetO(log n)基于红黑树,自动排序

典型应用场景

元素的不可变性

遍历方式

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

线程安全性

TreeSet 的排序问题

  • 用户ID集合(唯一性):HashSet
  • 最近访问记录(保持顺序):LinkedHashSet
  • 成绩排名(需要排序):TreeSet
  • 问题:如果 Set 集合中存储的是可变对象,修改其属性可能导致:
    • hashCode 值变化,导致无法正常查找
    • 比较结果变化,破坏 TreeSet 的有序性
  • 解决方案
    • 优先使用不可变对象(如 String、Integer)
    • 如果必须使用可变对象:
      • 确保关键属性不会改变
      • 修改后从集合中移除再重新添加
  • 增强 for 循环(最简单):

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

  • 迭代器(Iterator):

    Iterator<String> it = set.iterator();
    while (it.hasNext()) {System.out.println(it.next());
    }
    

  • Java 8+ Stream API

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

  • 问题:Set 接口的所有实现类(HashSet、LinkedHashSet、TreeSet)都是非线程安全的。
  • 解决方案
    • 使用 Collections.synchronizedSet() 包装:
      Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
      

    • 使用并发集合(如 ConcurrentHashMap 的 KeySet)
  • 注意事项:即使使用同步集合,复合操作(如检查再添加)仍需要额外同步。
  • 自然排序(元素实现 Comparable 接口)

    • 元素必须实现 Comparable 接口
    • 重写 compareTo() 方法
    • 示例:
      public class Student implements Comparable<Student> {@Overridepublic int compareTo(Student s) {return this.studentId.compareTo(s.studentId);}
      }
      

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

    • 创建 TreeSet 时指定比较器
    • 示例:
      TreeSet<Student> students = new TreeSet<>((s1, s2) -> s1.getAge() - s2.getAge()
      );
      

  • 唯一性规则:无论是自然排序还是自定义排序,比较结果为0的两个元素会被视为相同元素,不会被同时存入集合。

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

相关文章:

  • LangChain SQLChatMessageHistory:SQL数据库存储聊天历史详解
  • Day05 店铺营业状态设置 Redis
  • MQTTX使用wss的连接报错
  • Java -- List接口方法--遍历--ArrayList的注意事项
  • 贪心----4.划分字母区间
  • 方格网法土方计算不规则堆体
  • [ 前端JavaScript的事件流机制 ] - 捕获、冒泡及委托
  • 少数民族文字OCR识别技术实现及应用场景剖析
  • JMeter并发测试与多进程测试
  • __base__属性
  • ETCD的简介和使用
  • 42.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--网关集成认证(一)
  • 1513-map 的三种声明定义方式 使用方式
  • BN层:深度学习中的“数据稳定器”,如何解决训练难题?
  • 基于C#的二手服装交易网站的设计与实现/基于asp.net的二手交易系统的设计与实现/基于.net的闲置物品交易系统的设计与实现
  • 嵌入式Linux学习 -- 软件编程3
  • UNet改进(32):结合CNN局部建模与Transformer全局感知
  • Docker 101:面向初学者的综合教程
  • 【C#】从 Queue 到 ConcurrentQueue:一次对象池改造的实战心得
  • 激活函数篇(2):SwiGLU | GLU | Swish | ReLU | Sigmoid
  • 如何查看当前Redis的密码、如何修改密码、如何快速启动以及重启Redis (Windows)
  • 鹧鸪云:光伏施工流程管理的智能“导航仪”
  • 云平台监控-云原生环境Prometheus企业级监控实战
  • 【Redis与缓存预热:如何通过预加载减少数据库压力】
  • RoboNeo美图AI助手
  • 如何单独修改 npm 版本(不改变 Node.js 版本)
  • npm、pnpm、yarn区别
  • 深度解析Mysql的开窗函数(易懂版)
  • docker-compose安装ElasticSearch,ik分词器插件,kibana【超详细】
  • 夜莺开源监控,模板函数一览