Java Set的理解

一、Set 集合总览

在 Java 的集合框架中,Set 是一个重要的接口,它代表了一个不包含重复元素的集合。这种特性使得 Set 在许多场景下都有着独特的用途,比如去除重复数据、存储无序的唯一元素集合等。Set 接口有多个实现类,每个实现类都基于不同的数据结构,拥有各自的特点,本文将重点介绍 HashSet、LinkedHashSet、TreeSet 和 CopyOnWriteArraySet 这四个常用的实现类。

二、HashSet:高效无序去重

2.1 特性剖析

HashSet 是 Set 接口的典型实现类,它基于哈希表(实际上是 HashMap 实例)实现,具有以下显著特性:

  • 无序性:HashSet 中的元素没有特定的顺序,它们不是按照插入的顺序排列,也不会遵循任何自然的排序规则,每次遍历得到的元素顺序可能都不一样。
  • 不包含重复元素:这是 Set 接口的核心特性之一,HashSet 利用哈希算法和 equals 方法严格确保集合中不会出现重复的元素。当试图添加一个已经存在的元素时,添加操作将被忽略。
  • 允许 null 元素:HashSet 可以容纳一个 null 值作为集合中的元素,不过需要注意,只能有一个 null 元素存在。
  • 高效性能:基于哈希表的存储结构,使得 HashSet 在执行查找、插入和删除等操作时,具有非常高的效率,时间复杂度接近常数级别 O (1),在大数据量下优势尤为明显。

2.2 底层原理

HashSet 的底层哈希表结构本质上是一个数组加链表(在 Java 8 中,链表长度超过阈值 8 时会转换为红黑树以优化查询性能)的组合。当向 HashSet 中添加元素时,首先会调用元素的 hashCode () 方法获取哈希值,通过对哈希值进行一系列运算,确定元素在数组中的存储位置(桶索引)。若该桶为空,则直接将元素存入;若桶中已有元素,就会进一步调用 equals () 方法来判断元素是否真正相等。只有当两个元素的 hashCode 值相同且 equals 方法返回 true 时,才认定为重复元素,不进行添加。例如,有自定义类 Person,向 HashSet 添加多个 Person 对象时,需重写 hashCode 和 equals 方法,以确保根据对象的关键属性判断是否重复,避免因默认的对象内存地址比较方式导致错误的重复存储。

2.3 使用示例

以下是 HashSet 的一些常见操作示例:

java 复制代码
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetExample {
    public static void main(String[] args) {
        // 创建HashSet对象
        Set<String> hashSet = new HashSet<>();
        // 添加元素
        hashSet.add("apple");
        hashSet.add("banana");
        hashSet.add("orange");
        hashSet.add("apple"); // 重复元素,不会被添加
        // 删除元素
        hashSet.remove("banana");
        // 判断是否包含元素
        boolean contains = hashSet.contains("orange");
        System.out.println("是否包含orange:" + contains);
        // 获取元素个数
        int size = hashSet.size();
        System.out.println("元素个数:" + size);
        // 遍历HashSet
        System.out.println("使用迭代器遍历:");
        Iterator<String> iterator = hashSet.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
        System.out.println("使用增强for循环遍历:");
        for (String element : hashSet) {
            System.out.println(element);
        }
    }
}

在上述代码中,创建了一个 HashSet 并进行添加、删除、判断包含、获取大小以及遍历等操作。需要注意,遍历顺序是不确定的,因为 HashSet 不保证元素顺序。同时,在实际应用中,如果存储自定义对象,务必正确重写 hashCode 和 equals 方法,以保证 HashSet 对重复元素的准确判断。

三、LinkedHashSet:有序的 HashSet

3.1 独特特性

LinkedHashSet 是 HashSet 的子类,它继承了 HashSet 的所有特性,同时又额外具备一个显著特点:它使用链表维护元素的添加顺序。这使得 LinkedHashSet 在遍历集合时,元素的顺序与添加顺序保持一致,具有可预测的迭代次序。虽然在插入性能上略低于 HashSet,因为每次插入元素时需要额外维护链表的顺序,但在迭代访问全部元素时,性能表现良好,特别是当程序逻辑对元素顺序有要求时,LinkedHashSet 能很好地满足需求。

3.2 实现原理

LinkedHashSet 的底层数据结构是哈希表(数组 + 链表 / 红黑树)与双向链表的结合。哈希表用于保证元素的唯一性,其原理与 HashSet 相同,通过元素的 hashCode 值来确定存储位置,利用 equals 方法判断元素是否重复。而双向链表则用于记录元素的插入顺序,每当有新元素添加时,不仅将元素存入哈希表相应位置,还将其插入到链表的末尾,这样就完整地保存了元素的添加顺序。

3.3 代码演示

以下是 LinkedHashSet 的使用示例:

java 复制代码
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetExample {
    public static void main(String[] args) {
        // 创建LinkedHashSet对象
        Set<String> linkedHashSet = new LinkedHashSet<>();
        // 添加元素
        linkedHashSet.add("apple");
        linkedHashSet.add("banana");
        linkedHashSet.add("orange");
        linkedHashSet.add("apple"); // 重复元素,不会被添加
        // 删除元素
        linkedHashSet.remove("banana");
        // 判断是否包含元素
        boolean contains = linkedHashSet.contains("orange");
        System.out.println("是否包含orange:" + contains);
        // 获取元素个数
        int size = linkedHashSet.size();
        System.out.println("元素个数:" + size);
        // 遍历LinkedHashSet,可观察到元素按添加顺序输出
        System.out.println("使用迭代器遍历:");
        Iterator<String> iterator = linkedHashSet.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
        System.out.println("使用增强for循环遍历:");
        for (String element : linkedHashSet) {
            System.out.println(element);
        }
    }
}

在上述代码中,创建 LinkedHashSet 并执行一系列操作,与 HashSet 不同的是,遍历 LinkedHashSet 时,元素顺序始终是 "apple""banana""orange",这体现了它有序的特性,在实际应用中,如需要记录操作日志集合且保证日志顺序与执行顺序一致时,LinkedHashSet 就非常实用。

四、TreeSet:自动排序的集合

4.1 显著特性

TreeSet 是一个基于红黑树(Red - Black Tree)数据结构实现的有序集合,它实现了 NavigableSet 接口和 SortedSet 接口。TreeSet 的最大特点就是其元素的有序性,与 HashSet 的无序和 LinkedHashSet 的添加顺序不同,TreeSet 中的元素会自动按照一定的顺序排列,要么是元素的自然顺序(若元素类实现了 Comparable 接口),要么是依据创建 TreeSet 时传入的 Comparator 比较器所指定的顺序。同时,它也严格遵循 Set 接口的规则,不允许存在重复元素,一旦试图添加重复元素,添加操作将失败。

4.2 排序规则

元素的排序规则主要通过以下两种方式确定:

  • 自然排序:当元素类实现了 java.lang.Comparable 接口时,TreeSet 会调用元素的 compareTo () 方法来比较元素之间的大小关系,进而按照升序排列元素。例如,对于 Integer、String 等 Java 内置类,它们已经实现了 Comparable 接口,所以可以直接放入 TreeSet 中进行自然排序。像存储一系列整数的 TreeSet,添加元素后,遍历输出会自动按照数字从小到大的顺序展示。
  • 定制排序:若元素类没有实现 Comparable 接口,或者需要自定义特殊的排序逻辑,就可以在创建 TreeSet 时传入一个实现了 java.util.Comparator 接口的比较器。这个比较器的 compare () 方法定义了具体的排序规则,通过返回值来表明两个元素的大小关系(返回值小于 0 表示第一个元素小于第二个元素,等于 0 表示两元素相等,大于 0 表示第一个元素大于第二个元素)。例如,要按照字符串的长度对一组字符串进行排序,就可以创建一个自定义的 Comparator 并传入 TreeSet 的构造函数。

TreeSet 的无参构造函数默认使用自然排序方式,如果元素无法比较(未实现 Comparable 接口且未提供 Comparator),在添加元素时会抛出 ClassCastException 异常。

4.3 操作实例

以下是 TreeSet 的一些操作示例:

java 复制代码
import java.util.Iterator;
import java.util.TreeSet;
public class TreeSetExample {
    public static void main(String[] args) {
        // 创建TreeSet对象,使用默认自然排序(元素需实现Comparable接口)
        TreeSet<Integer> treeSet = new TreeSet<>();
        // 添加元素
        treeSet.add(5);
        treeSet.add(3);
        treeSet.add(8);
        treeSet.add(1);
        treeSet.add(3); // 重复元素,不会被添加
        // 遍历TreeSet,元素将按升序输出
        System.out.println("使用迭代器遍历:");
        Iterator<Integer> iterator = treeSet.iterator();
        while (iterator.hasNext()) {
            Integer element = iterator.next();
            System.out.println(element);
        }
        System.out.println("使用增强for循环遍历:");
        for (Integer element : treeSet) {
            System.out.println(element);
        }
        // 判断是否包含元素
        boolean contains = treeSet.contains(5);
        System.out.println("是否包含5:" + contains);
        // 获取元素个数
        int size = treeSet.size();
        System.out.println("元素个数:" + size);
        // 删除元素
        treeSet.remove(3);
        // 获取集合中的第一个元素
        Integer first = treeSet.first();
        System.out.println("第一个元素:" + first);
        // 获取集合中的最后一个元素
        Integer last = treeSet.last();
        System.out.println("最后一个元素:" + last);
    }
}

在上述代码中,创建了一个存储整数的 TreeSet,元素自动按升序排列,展示了添加、遍历、判断包含、获取大小、删除以及获取首尾元素等操作。需注意,TreeSet 不是线程安全的,如果在多线程环境下并发操作,可能会导致数据不一致等问题,必要时需要通过外部同步手段(如使用 Collections.synchronizedSortedSet 方法对 TreeSet 进行包装)来保证线程安全。

五、CopyOnWriteArraySet:线程安全的选择

5.1 优势特性

CopyOnWriteArraySet 是 Java 并发包(java.util.concurrent)中的一个线程安全的 Set 实现类,它可以看作是线程安全的 HashSet。其具有以下突出特性:

  • 线程安全:这是它最重要的特性,在多线程环境下,多个线程可以同时对 CopyOnWriteArraySet 进行读操作,而无需额外的同步措施,写操作(如添加、删除元素)也能保证数据的一致性,不会出现数据污染、丢失等并发问题。这得益于其底层采用的写时复制(Copy-On-Write)策略。
  • 适用于读多写少场景:由于可变操作(写操作)需要复制整个底层数组,开销较大,而读操作不需要加锁,性能极高,所以当集合的读操作远远多于写操作时,CopyOnWriteArraySet 能展现出良好的性能表现,避免频繁的锁竞争带来的性能损耗。
  • 迭代器遍历快且无冲突:迭代器遍历集合时,依赖于创建迭代器那一刻的数组快照,后续即使集合被修改,迭代器也不受影响,不会抛出 ConcurrentModificationException 异常,能快速、稳定地遍历集合,不会与其他线程的写操作产生冲突。

5.2 实现机理

CopyOnWriteArraySet 的底层是通过 CopyOnWriteArrayList 来实现的。它包含一个 CopyOnWriteArrayList 类型的成员变量,利用 CopyOnWriteArrayList 已经实现的线程安全机制以及数组操作特性来构建自身功能。当执行添加元素操作时,调用 add (E e) 方法,实际上是委托给内部的 CopyOnWriteArrayList 的 addIfAbsent (E e) 方法,这个方法会先检查要添加的元素是否已存在于当前数组快照中,若不存在才执行添加。例如,在多线程环境下,多个线程同时尝试向 CopyOnWriteArraySet 添加元素,第一个获取锁的线程会复制当前数组,在副本上检查元素是否重复并添加,添加完成后将原数组引用指向新数组,后续线程重复此过程,确保不会添加重复元素,保证了 Set 的特性。

5.3 多线程场景示例

以下是一个多线程场景下使用 CopyOnWriteArraySet 的示例:

java 复制代码
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class CopyOnWriteArraySetExample {
    public static void main(String[] args) {
        // 创建CopyOnWriteArraySet对象
        Set<String> copyOnWriteArraySet = new CopyOnWriteArraySet<>();
        // 创建并启动多个线程对集合进行操作
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                copyOnWriteArraySet.add("Thread1-" + i);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                copyOnWriteArraySet.add("Thread2-" + i);
            }
        });
        thread1.start();
        thread2.start();
        // 等待线程执行完毕
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 遍历集合
        Iterator<String> iterator = copyOnWriteArraySet.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
    }
}

与之对比,如果将上述代码中的 CopyOnWriteArraySet 换成 HashSet,在多线程并发添加元素并遍历的过程中,很容易出现 ConcurrentModificationException 异常,因为 HashSet 是非线程安全的,在遍历过程中若其他线程修改了集合结构,就会导致迭代器失效。而 CopyOnWriteArraySet 通过其独特的实现机制,保证了在多线程环境下的稳定运行,适用于如配置信息缓存、系统白名单等读多写少且对线程安全有要求的场景。

六、总结与选择建议

在 Java 的集合框架中,HashSet、LinkedHashSet、TreeSet 和 CopyOnWriteArraySet 各有千秋,它们适用于不同的业务场景:

  • HashSet:适用于对元素顺序没有要求,追求高效的添加、删除和查找操作,且数据量较大的场景。例如在大数据去重、缓存数据存储(不考虑数据顺序时)等场景下,HashSet 能够以出色的性能快速处理。像电商系统中对大量用户浏览过的商品 ID 进行去重,HashSet 可以快速过滤重复元素,节省存储空间。
  • LinkedHashSet:当需要保持元素的插入顺序,同时又希望利用 HashSet 的快速存取特性时,LinkedHashSet 是不二之选。在一些需要记录操作顺序的日志系统、缓存淘汰策略实现(按照插入顺序淘汰)等方面,它能在保证有序的前提下提供高效操作。比如记录用户操作的历史记录集合,按操作发生顺序存储,方便后续回溯查看。
  • TreeSet:如果业务场景对元素的排序有严格要求,无论是自然排序还是定制排序,TreeSet 都能满足。在数据统计分析场景,如对学生成绩排序、员工工资排序等,或者实现范围查询(利用 TreeSet 的导航方法)时,TreeSet 的有序特性使其能够轻松应对。例如,查找成绩排名前 10% 的学生信息,TreeSet 可以快速定位到对应的元素范围。
  • CopyOnWriteArraySet:专为多线程环境下读多写少的场景设计,它能保证线程安全,避免并发修改异常,同时在读操作频繁的场景下性能卓越。像系统中的全局配置信息缓存,多个线程可能随时读取配置,偶尔修改配置,使用 CopyOnWriteArraySet 既能保证数据一致性,又不会因频繁加锁影响读性能。

总之,在实际编程中,需要根据具体的业务需求、数据特点以及并发场景,综合考虑选择最合适的 Set 实现类,以充分发挥 Java 集合框架的优势,提升程序的性能与稳定性。

相关推荐
大梦百万秋1 小时前
大中厂面试经验分享:如何使用消息队列(MQ)解决系统问题
经验分享·面试·职场和发展
啊烨疯狂学java5 小时前
1231java面经md
java·算法·面试·排序算法
HEU_firejef6 小时前
面试经典150题——矩阵
面试·矩阵·哈希算法
Pandaconda7 小时前
【Golang 面试题】每日 3 题(七)
开发语言·笔记·后端·面试·职场和发展·golang·go
TANGLONG2229 小时前
【初阶数据结构与算法】排序算法总结篇(每个小节后面有源码)(直接插入、希尔、选择、堆、冒泡、快速、归并、计数以及非递归快速、归并排序)
java·c语言·数据结构·c++·算法·面试·排序算法
测试界茜茜16 小时前
14:00面试,14:08就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
程序员汤圆17 小时前
【软件测试面试】银行项目测试面试题+答案(二)
软件测试·面试·职场和发展·测试用例
crayons3224221 小时前
React 结合实际项目深度优化:提升性能与开发体验的最佳实践
前端·javascript·面试
美式小田1 天前
硬件工程师面试题 21-30
嵌入式硬件·面试·硬件工程师
互联网杂货铺1 天前
2025常见的软件测试面试题
自动化测试·软件测试·python·测试工具·面试·职场和发展·测试用例