【从0开始学习Java | 第17篇】集合(中-Set部分)

文章目录

  • Java集合之Set:无序不重复的元素容器
    • 一、Set接口的核心特性
    • 二、常用实现类及底层原理
        1. HashSet:基于哈希表的高效实现
        1. LinkedHashSet:保留插入顺序的哈希实现
        1. TreeSet:基于红黑树的排序实现
    • 三、实现类对比与选择建议
    • 四、使用注意事项
    • 总结

Java集合之Set:无序不重复的元素容器

在Java集合框架中,Set是与List并列的重要接口,它以"无序且元素不重复"为核心特性,在数据去重、快速查找等场景中被广泛应用。本文将深入解析Set接口及其常用实现类,帮助你理解它们的底层原理与适用场景。

一、Set接口的核心特性

Set继承自Collection接口,但其行为与List有显著区别:

  • 无序性 :元素的存储顺序与插入顺序无关(特殊实现类如LinkedHashSet除外),不能通过索引访问元素。
  • 唯一性/不重复 :集合中不允许存在重复元素,判断重复的依据是equals()方法,若元素重写了equals(),通常需同时重写hashCode()以保证一致性。
  • 无索引 :没有类似get(int index)的方法,遍历需通过迭代器(Iterator)或增强for循环。

Set接口的常用方法与Collection基本一致,如add()remove()contains()size()等,核心差异体现在实现类对"去重"和"查找效率"的不同优化上。

二、常用实现类及底层原理

1. HashSet:基于哈希表的高效实现


1. JDK8以后,当链表长度超过8,而且数组长度大于等于64时,自动转换为红黑树
2. 如果集合中存储的是自定义对象,必须重写hashCodeequals方法才能用来比较属性值是否相同

在知道了HashSet的底层原理后,我们就可以回答以下三个问题了:

1. HashSet为什么存和取的顺序不一样?

因为HashSet可能是数组+链表+红黑树的结合体,而取的时候,是从最小的地址值开始遍历,而先放进去的元素的地址值不一定就小,以上都造成了HashSet的存取顺序不同。

2. HashSet为什么没有索引?

因为HashSet是数组+链表+红黑树的结合体,若存在索引,无法分配索引,难道同一个链表上和同一个红黑树上的元素都用同一个索引码,因此便取消了HashSet的索引。

3. HashSet是利用什么机制保证数据去重的?

首先利用 HashCode方法来确定元素的位置,再通过equals方法来判断属性值是否重复【重写了HashCode 和 equals 方法】。

HashSetSet最常用的实现类,底层通过哈希表(HashMap) 实现,其核心特性如下:

  • 存储原理 :借助HashMapkey存储元素(value为固定常量),利用哈希表的特性保证元素唯一。
  • 去重逻辑 :添加元素时,先通过hashCode()计算哈希值,若哈希值不同则直接存储;若哈希值相同,再通过equals()判断是否为同一元素,相同则拒绝添加。
  • 性能
    • add()、remove()、contains()等操作的平均时间复杂度为O(1),效率极高。
    • 无序性:元素存储位置由哈希值决定,与插入顺序无关。
  • 注意事项
    • 元素需重写hashCode()equals(),否则可能导致重复元素存入(如自定义对象未重写方法时,默认使用地址值判断【重要的事反复强调】)。
    • 线程不安全,多线程环境需手动同步(如使用Collections.synchronizedSet())。

示例代码

java 复制代码
Set<String> hashSet = new HashSet<>();
hashSet.add("apple");
hashSet.add("banana");
hashSet.add("apple"); // 重复元素,添加失败
System.out.println(hashSet); // 输出可能为 [banana, apple](顺序不确定)

2. LinkedHashSet:保留插入顺序的哈希实现

LinkedHashSet继承自HashSet,底层通过LinkedHashMap 实现,在哈希表基础上增加了双向链表,用于记录元素的插入顺序,因此具备以下特性:

  • 有序性:遍历元素时按插入顺序返回,解决了HashSet的无序问题。
  • 性能
    • 插入和删除效率略低于HashSet(因需维护链表),但遍历效率更高。
    • 时间复杂度仍为O(1) (平均),与HashSet接近。
  • 适用场景:需要去重且保留插入顺序的场景(如日志记录、历史操作跟踪)。

示例代码

java 复制代码
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("first");
linkedHashSet.add("second");
linkedHashSet.add("first"); // 去重
System.out.println(linkedHashSet); // 输出 [first, second](顺序与插入一致)

3. TreeSet:基于红黑树的排序实现

TreeSet底层通过红黑树(自平衡二叉查找树) 实现,核心特性是元素可排序

  • 两种排序方式
    • 自然排序:元素实现Comparable接口,重写compareTo()方法(如IntegerString默认支持)。

代码示例:

java 复制代码
public class Student implements Comparable<Student> {
   private String name;
  private int age;
   
   @Override
   public int compareTo(Student o) {
   //this:表示当前要添加的元素
   //o:表示已经在红黑树存在的元素
       return this.age - o.age; // 按年龄升序排列
   }
}

排序规则图示:

适用场景

  • 元素类有明确且唯一的默认排序逻辑(如数字按大小、字符串按字典序)。
  • 排序规则需要在多处复用,且不会频繁变更。
  • 元素类是自定义类,允许修改其代码以实现 Comparable 接口。

定制排序 / 比较器排序:创建TreeSet时传入Comparator对象,自定义排序逻辑。

java 复制代码
// 按字符串长度排序
// lambda表达式版:
TreeSet<String> ts= new TreeSet<>((o1, o2) -> o1.length() - o2.length());

// 原始版本:
TreeSet<String> ts= new TreeSet<>(new Comparator<String>() {
    @Override
    // o1:表示当前要添加的元素
    // o2:表示已经在红黑树存在的元素
    public int compare(String o1, String o2) {
        return o1.length() - o2.length();
    }
});

适用场景

  • 元素类无法修改(如 String、Integer 等 JDK 内置类),无法实现 Comparable。
  • 需要临时改变排序规则(同一批元素在不同场景下按不同规则排序)。
  • 元素类已有默认排序,但需要覆盖默认规则(如数字默认升序,需临时按降序排列)。

  • 去重逻辑 :通过排序规则判断元素是否重复(compareTo()返回0则视为重复,而非 equals() 方法,因为其底层为红黑树,非哈希表)。
  • 性能
    • add()、remove()、contains()操作的时间复杂度为O(log n),适合需要频繁排序和查找的场景。
    • 遍历元素时按排序顺序返回,无需额外排序操作。
  • 注意事项
    • 元素必须可比较(否则会抛出ClassCastException)。
    • 线程不安全,多线程环境需同步处理。

示例代码

java 复制代码
// 自然排序(String默认按字典序)
Set<String> treeSet = new TreeSet<>();
treeSet.add("orange");
treeSet.add("apple");
treeSet.add("banana");
System.out.println(treeSet); // 输出 [apple, banana, orange](按字典序排序)

// 定制排序(整数降序)
Set<Integer> customTreeSet = new TreeSet<>((a, b) -> b - a);
customTreeSet.add(3);
customTreeSet.add(1);
customTreeSet.add(2);
System.out.println(customTreeSet); // 输出 [3, 2, 1]

使用原则默认使用第一种,如果第一种不能满足当前需求,就使用第二种

三、实现类对比与选择建议

三者均不重复,且无索引   哈希表 => 数组+单向链表+红黑树

实现类 底层结构 有序性 去重依据 平均时间复杂度 适用场景
HashSet 哈希表 无序 hashCode() + equals() O(1) 高效去重,不关心顺序
LinkedHashSet 哈希表+双向链表 有序,插入顺序 hashCode() + equals() O(1) 去重且需保留插入顺序
TreeSet 红黑树 可排序 compareTo()/Comparator O(log n) 去重且需排序或频繁范围查询

四、使用注意事项

  1. 元素的不可变性 :若元素是可变对象,修改其属性可能导致哈希值或排序位置变化,引发Set内部状态混乱(如HashSet中元素修改后可能无法被正确查找)。建议使用不可变对象(如StringInteger)作为Set元素。

  2. hashCode()与equals()的一致性

    • a.equals(b) == true,则a.hashCode()必须等于b.hashCode()
    • 重写时尽量保证哈希值分布均匀(减少哈希冲突),避免影响HashSet性能。
  3. 线程安全 :所有Set实现类均线程不安全,多线程并发修改时需使用Collections.synchronizedSet()包装,或直接使用ConcurrentHashMap(JDK 1.8+)的newKeySet()方法创建线程安全的Set

总结

HashSet追求高效,LinkedHashSet兼顾顺序,TreeSet专注排序。理解它们的底层原理(哈希表、红黑树)和特性差异,能帮助你在实际开发中选择最合适的容器,提升代码效率与可读性。


如果我的内容对你有帮助,请 点赞 , 评论 , 收藏 。创作不易,大家的支持就是我坚持下去的动力!

相关推荐
num_killer5 小时前
小白的Langchain学习
java·python·学习·langchain
wdfk_prog6 小时前
[Linux]学习笔记系列 -- hashtable
linux·笔记·学习
期待のcode6 小时前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐6 小时前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲6 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红6 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥6 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v6 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地7 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209257 小时前
Guava Cache 原理与实战
java·后端·spring