找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2123

积分

0

好友

294

主题
发表于 昨天 07:37 | 查看: 5| 回复: 0

Java 编程中,Set集合是处理唯一元素的重要工具。本文将深入探讨Set接口及其常见实现类的使用、区别以及如何在多线程环境下安全操作。

一、Set接口

Set接口代表一个无序的集合,它不允许重复元素。具体来说,Set存储一组唯一且无序的对象(尝试添加重复元素时,add方法会返回false)。它不保证迭代顺序的恒定性,但某些实现类如TreeSet会按元素排序,LinkedHashSet会按插入顺序排序。Set接口允许一个null元素,但注意TreeSet等实现可能禁止null

Set接口继承自Collection,其典型实现包括HashSetLinkedHashSetTreeSet

二、HashSet实现类

HashSet基于哈希表(实际是HashMap)实现,利用哈希算法存储元素。它不保证元素顺序,但提供了优异的存取和查找性能,允许null元素,并且在多线程环境下是非线程安全的。

当向HashSet添加元素时,它会调用对象的hashCode()方法获取哈希值,并据此决定存储位置。如果两个元素通过equals()比较为true,但hashCode()返回值不同,HashSet会将它们存储在不同位置,仍可添加成功。换言之,HashSet判断元素相等的标准是:equals()返回truehashCode()返回值相等。这依赖于元素正确重写hashCode()equals()方法以确保唯一性。

那么,如果不重写这些方法会发生什么?hashCode()Object类的方法,默认返回对象在内存中的唯一标识。如果只重写hashCode(),当向HashSet添加多个属性相同的对象时,可能无法达到去重效果。同样,如果只重写equals()方法(默认比较对象地址),也无法实现完全去重。只有同时重写hashCode()equals()方法,HashSet才能正确去重,这是 算法/数据结构 中对象比较的基础。

以下是一个代码示例,展示如何通过重写这两个方法来实现Student对象的去重:

public class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    //判断两个对象是否相等,对象是否存在,对象的name和age是否相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                Objects.equals(name, student.name);
    }

    //返回对象的name和age的hash值
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    public static void main(String[] args) {
        Student s1 = new Student("刘亦菲", 23);
        Student s2 = new Student("杨幂", 24);
        Student s3 = new Student("刘亦菲", 23);
        Student s4 = new Student("邓紫棋", 25);
        Student s5 = new Student("刘诗诗", 29);
        Student s6 = new Student("刘诗诗", 30);
        HashSet<Student> hashSet = new HashSet<>();
        hashSet.add(s1);
        hashSet.add(s2);
        hashSet.add(s3);
        hashSet.add(s4);
        hashSet.add(s5);
        hashSet.add(s6);
        for (Student student : hashSet) {
            System.out.println(student.getName()+"=="+student.getAge());
        }
    }
}

需要注意的是,向HashSet添加可变对象时必须小心。如果修改了集合中的对象,可能导致该对象与其他对象相等,从而影响HashSet的正常访问。

三、LinkedHashSet实现类

LinkedHashSet继承自HashSet。它同样根据元素的hashCode值决定存储位置,但额外使用链表维护元素的插入次序。这使得遍历LinkedHashSet时,元素会按照添加顺序被访问。输出元素时,顺序总是与添加顺序一致。它同样不允许重复元素,允许null,并且在多线程下是非线程安全的。

以下是LinkedHashSet的示例,使用与前文相同的Student类:

public static void main(String[] args) {
    Student s1 = new Student("刘亦菲", 23);
    Student s2 = new Student("杨幂", 24);
    Student s3 = new Student("刘亦菲", 23);
    Student s4 = new Student("邓紫棋", 25);
    Student s5 = new Student("刘诗诗", 29);
    Student s6 = new Student("刘诗诗", 30);
    LinkedHashSet<Student> hashSet = new LinkedHashSet<>();
    hashSet.add(s1);
    hashSet.add(s2);
    hashSet.add(s3);
    hashSet.add(s4);
    hashSet.add(s5);
    hashSet.add(s6);
    for (Student student : hashSet) {
        System.out.println(student.getName()+"=="+student.getAge());
    }
}

四、TreeSet实现类

TreeSet基于TreeMap实现,使用红黑树结构存储元素。它实现了NavigableSet接口,元素会按照自然顺序或指定的比较器(Comparator)进行排序。TreeSet不允许null元素(因为需要比较大小),并且在多线程下是非线程安全的。

TreeSet添加元素时,它会调用对象的compareTo()方法(或比较器)与容器中其他元素比较大小,然后在红黑树中找到合适位置。如果两个元素相等,则新元素无法加入。元素必须实现Comparable接口,或在构造TreeSet时传入Comparator,否则会抛出ClassCastException。排序规则在使用过程中不能改变,否则会导致集合状态混乱。

1. 元素实现Comparable接口

public class Student implements Comparable<Student>{
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Student o) {
        //比较age
        int num = this.age - o.age;
        //如果age相等则比较name长度
        int num1 = num == 0 ? this.name.length() - o.name.length() : num;
        //如果前两者都相等则比较name字符串
        int num2 = num1 == 0 ? this.name.compareTo(o.name) : num1;
        return num2;
    }

    public static void main(String[] args) {
        Student s1 = new Student("刘亦菲", 23);
        Student s2 = new Student("杨幂", 24);
        Student s3 = new Student("刘亦菲", 23);
        Student s4 = new Student("邓紫棋", 25);
        Student s5 = new Student("刘诗诗", 29);
        Student s6 = new Student("刘诗诗", 30);
        Student s7 = new Student("刘试试", 30);
        TreeSet<Student> hashSet = new TreeSet<>();
        hashSet.add(s1);
        hashSet.add(s2);
        hashSet.add(s3);
        hashSet.add(s4);
        hashSet.add(s5);
        hashSet.add(s6);
        hashSet.add(s7);
        for (Student student : hashSet) {
            System.out.println(student.getName()+"=="+student.getAge());
        }
    }
}

2. 构造TreeSet时传入Comparator

public class Student {
    // ... 属性、getter、setter等同上,无需实现Comparable

    public static void main(String[] args) {
        Student s1 = new Student("刘亦菲", 23);
        Student s2 = new Student("杨幂", 24);
        Student s3 = new Student("刘亦菲", 23);
        Student s4 = new Student("邓紫棋", 25);
        Student s5 = new Student("刘诗诗", 29);
        Student s6 = new Student("刘诗诗", 30);
        Student s7 = new Student("刘试试", 30);
        TreeSet<Student> hashSet = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                // 比较age
                int num = o1.age - o2.age;
                //如果age相等则比较name长度
                int num1 = num == 0 ? o1.name.length() - o2.name.length() : num;
                //如果前两者都相等则比较name字符串
               int num2 = num1 == 0 ? o1.name.compareTo(o2.name) : num1;
               return num2;
           }
        });
        hashSet.add(s1);
        hashSet.add(s2);
        hashSet.add(s3);
        hashSet.add(s4);
        hashSet.add(s5);
        hashSet.add(s6);
        hashSet.add(s7);
        for (Student student : hashSet) {
            System.out.println(student.getName()+"=="+student.getAge());
        }
    }
}

3. TreeSet的NavigableSet接口方法示例

TreeSet实现了NavigableSet接口,提供了一系列范围查询和操作:

public static void main(String[] args) {
  TreeSet<Integer> set = new TreeSet<>(Arrays.asList(1, 3, 5, 7, 9));

  // 范围查询
  System.out.println("First: " + set.first()); // 1
  System.out.println("Last: " + set.last()); // 9
  System.out.println("Ceiling(4): " + set.ceiling(4)); // 5(>=4的最小元素)
  System.out.println("Floor(4): " + set.floor(4)); // 3(<=4的最大元素)
  System.out.println("Higher(4): " + set.higher(4)); // 5(>4的最小元素)
  System.out.println("Lower(4): " + set.lower(4)); // 3(<4的最大元素)

  // 子集操作
  System.out.println("HeadSet(5): " + set.headSet(5)); // [1, 3] (<5)
  System.out.println("HeadSet(5, true): " + set.headSet(5, true)); // [1, 3, 5] (<=5)
  System.out.println("TailSet(5): " + set.tailSet(5)); // [5, 7, 9] (>=5)
  System.out.println("SubSet(3, 7): " + set.subSet(3, 7)); // [3, 5] (>=3, <7)

  // 获取和移除
  System.out.println("PollFirst: " + set.pollFirst()); // 移除并返回第一个元素 1
  System.out.println("PollLast: " + set.pollLast()); // 移除并返回最后一个元素 9
}

五、EnumSet实现类

EnumSet是专为枚举类型设计的集合类,所有元素都必须是指定枚举类型的值。EnumSet元素有序(按枚举定义顺序),内部以位向量形式存储,内存占用小且效率高。它不允许加入null元素。

// EnumSet 专为枚举类型设计
enum Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public class EnumSetDemo{
    public static void main(String[] args) {
        // 创建 EnumSet
        EnumSet<Day> weekdays = EnumSet.allOf(Day.class);  // 所有枚举值
        EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
        EnumSet<Day> workdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);
        EnumSet<Day> empty = EnumSet.noneOf(Day.class);

        // 添加元素
        weekend.add(Day.FRIDAY); // 现在包含周五、周六、周日

        // 删除元素
        weekdays.remove(Day.SUNDAY);

        // 集合运算
        EnumSet<Day> allDays = EnumSet.allOf(Day.class);
        EnumSet<Day> weekendDays = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
        EnumSet<Day> workDays = EnumSet.complementOf(weekendDays);  // 补集

        // 交集、并集、差集
        EnumSet<Day> set1 = EnumSet.of(Day.MONDAY, Day.TUESDAY);
        EnumSet<Day> set2 = EnumSet.of(Day.TUESDAY, Day.WEDNESDAY);

        set1.addAll(set2); // 并集
        set1.retainAll(set2); // 交集
        set1.removeAll(set2); // 差集

        // 遍历
        for (Day day : weekendDays) {
            System.out.println(day);
        }
    }
}

六、线程安全的Set实现

HashSetLinkedHashSetTreeSet都是线程不安全的。为什么?因为它们的添加元素方法底层调用了非线程安全的Map操作(如HashMap.put)。在多线程环境下,我们需要采取额外措施来保证线程安全。

1. 使用Collections.synchronizedSet

Collections.synchronizedSet(Set<T> s)方法可以返回一个线程安全的Set包装器,但它在所有操作(读和写)上都进行同步加锁,在高并发场景下可能成为性能瓶颈。

// Collections.synchronizedSet 源码示例
public boolean add(E e) {
    synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
    synchronized (mutex) {return c.remove(o);}
}
// ... 其他方法类似,均使用 synchronized 块

2. 使用CopyOnWriteArraySet

CopyOnWriteArraySet基于CopyOnWriteArrayList实现,适用于读多写少的并发场景。它在写入时复制底层数组,因此读取操作完全无锁,性能极高。

// CopyOnWriteArraySet 构造方法源码
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

总结与选择建议

Set是Java集合框架的核心组件,合理选择能提升代码可读性和性能。以下是一些实用建议:

  • 快速查找,不关心顺序:使用HashSet
  • 需要维护插入顺序:使用LinkedHashSet
  • 需要元素排序:使用TreeSet
  • 枚举类型集合:使用EnumSet以获得最佳性能。
  • 多线程环境
    • 读多写少:考虑CopyOnWriteArraySet
    • 通用场景:使用Collections.synchronizedSet包装,或考虑ConcurrentHashMap.newKeySet()

掌握这些Set实现类的特性和适用场景,能帮助你在实际开发中做出更优的选择。希望本文能为你理清Java Set集合的使用思路。想了解更多技术干货和实战经验,欢迎访问云栈社区与广大开发者交流探讨。




上一篇:区块链技术解析:从状态机到虚拟机的计算机本质
下一篇:技术总监因项目被撤离职?从职场八卦聊到LeetCode缺失区间算法
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 22:08 , Processed in 0.208090 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表