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

发布于:2025-09-12 ⋅ 阅读:(14) ⋅ 点赞:(0)

在这里插入图片描述

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())。

示例代码

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接近。
  • 适用场景:需要去重且保留插入顺序的场景(如日志记录、历史操作跟踪)。

示例代码

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默认支持)。

代码示例:

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对象,自定义排序逻辑。

// 按字符串长度排序
// 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)。
    • 线程不安全,多线程环境需同步处理。

示例代码

// 自然排序(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专注排序。理解它们的底层原理(哈希表、红黑树)和特性差异,能帮助你在实际开发中选择最合适的容器,提升代码效率与可读性。


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


网站公告

今日签到

点亮在社区的每一天
去签到