Java集合框架(三):Set接口深度解析与HashSet、TreeSet、LinkedHashSet对比

一、Set接口:不重复元素的集合

1.1 Set接口的核心特性

Set接口是Collection的子接口,它具有以下核心特征:

  • 唯一性:不允许重复元素(基于equals()和hashCode()判断)

  • 无序性:大多数实现不保证元素的顺序

  • 允许null元素:大多数实现允许一个null元素(TreeSet除外)

1.2 Set接口的方法体系

java 复制代码
import java.util.*;

public class SetInterfaceDemo {
    public static void main(String[] args) {
        // Set没有新增方法,完全继承Collection接口
        Set<String> set = new HashSet<>();
        
        // 基本操作
        set.add("Java");
        set.add("Python");
        set.add("Java"); // 重复元素不会被添加
        
        System.out.println("Set大小: " + set.size()); // 2
        System.out.println("是否包含Java: " + set.contains("Java")); // true
        
        // 批量操作
        Set<String> anotherSet = new HashSet<>();
        anotherSet.add("C++");
        anotherSet.add("JavaScript");
        
        set.addAll(anotherSet);
        System.out.println("并集后: " + set);
        
        // 交集操作
        set.retainAll(Arrays.asList("Java", "Python"));
        System.out.println("交集后: " + set);
        
        // Set的遍历(与Collection相同)
        System.out.println("\n遍历Set:");
        for (String language : set) {
            System.out.println(language);
        }
    }
}

二、HashSet:基于哈希表的Set实现

2.1 HashSet的内部结构与原理

java 复制代码
// HashSet的简化内部结构示意
public class HashSet<E> {
    // 核心:使用HashMap存储元素
    private transient HashMap<E, Object> map;
    
    // 虚拟值,用于作为HashMap的值
    private static final Object PRESENT = new Object();
    
    // 构造函数
    public HashSet() {
        map = new HashMap<>(); // 默认初始容量16,负载因子0.75
    }
    
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    
    // 添加元素:实际是向HashMap添加键值对
    public boolean add(E e) {
        return map.put(e, PRESENT) == null;
    }
    
    // 其他方法都是委托给HashMap
}

2.2 HashSet的哈希机制

java 复制代码
import java.util.*;

class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 重写equals和hashCode是使用HashSet的关键
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        // 使用Objects.hash()自动生成hashCode
        return Objects.hash(name, age);
    }
    
    @Override
    public String toString() {
        return name + "(" + age + ")";
    }
}

public class HashSetHashCodeDemo {
    public static void main(String[] args) {
        System.out.println("=== HashSet的哈希机制 ===");
        
        // 场景1:正确重写equals和hashCode
        Set<Person> personSet = new HashSet<>();
        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Alice", 25);
        
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // true
        System.out.println("p1.hashCode() == p2.hashCode(): " + 
                          (p1.hashCode() == p2.hashCode())); // true
        
        personSet.add(p1);
        personSet.add(p2); // 不会添加,因为被认为是重复元素
        
        System.out.println("正确实现后的Set大小: " + personSet.size()); // 1
        
        // 场景2:没有重写equals和hashCode
        class BadPerson {
            String name;
            int age;
            
            BadPerson(String name, int age) {
                this.name = name;
                this.age = age;
            }
        }
        
        Set<BadPerson> badSet = new HashSet<>();
        BadPerson bp1 = new BadPerson("Bob", 30);
        BadPerson bp2 = new BadPerson("Bob", 30);
        
        badSet.add(bp1);
        badSet.add(bp2); // 会被添加,因为使用Object的hashCode和equals
        
        System.out.println("\n错误实现后的Set大小: " + badSet.size()); // 2
        
        // 场景3:哈希冲突演示
        System.out.println("\n=== 哈希冲突与链表 ===");
        Set<Integer> intSet = new HashSet<>();
        
        // 查看HashMap的桶结构(通过反射)
        for (int i = 0; i < 20; i++) {
            intSet.add(i);
        }
        
        // 负载因子和扩容
        System.out.println("HashSet的负载因子默认值: 0.75");
        System.out.println("当元素数量达到容量*负载因子时,会自动扩容");
    }
}

2.3 HashSet的性能特点与使用场景

java 复制代码
import java.util.*;

public class HashSetPerformance {
    public static void main(String[] args) {
        System.out.println("=== HashSet性能分析 ===");
        
        // 性能测试:添加、查找、删除
        int size = 100000;
        HashSet<Integer> hashSet = new HashSet<>(size);
        
        // 1. 添加性能
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            hashSet.add(i);
        }
        long addTime = System.currentTimeMillis() - start;
        System.out.printf("添加%d个元素耗时: %d ms%n", size, addTime);
        
        // 2. 查找性能
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            hashSet.contains((int) (Math.random() * size));
        }
        long searchTime = System.currentTimeMillis() - start;
        System.out.printf("查找10000次耗时: %d ms%n", searchTime);
        
        // 3. 删除性能
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            hashSet.remove(i);
        }
        long removeTime = System.currentTimeMillis() - start;
        System.out.printf("删除10000个元素耗时: %d ms%n", removeTime);
        
        // 4. 内存占用
        Runtime runtime = Runtime.getRuntime();
        runtime.gc();
        long memoryBefore = runtime.totalMemory() - runtime.freeMemory();
        
        HashSet<String> largeSet = new HashSet<>();
        for (int i = 0; i < 100000; i++) {
            largeSet.add("String" + i);
        }
        
        runtime.gc();
        long memoryAfter = runtime.totalMemory() - runtime.freeMemory();
        System.out.printf("\n存储100000个字符串的内存占用: %.2f MB%n",
                         (memoryAfter - memoryBefore) / 1024.0 / 1024.0);
    }
}

三、TreeSet:基于红黑树的有序Set

3.1 TreeSet的内部结构与原理

java 复制代码
// TreeSet的简化内部结构示意
public class TreeSet<E> {
    // 核心:使用TreeMap存储元素
    private transient NavigableMap<E, Object> m;
    
    // 虚拟值
    private static final Object PRESENT = new Object();
    
    // 构造函数
    public TreeSet() {
        m = new TreeMap<>(); // 自然排序
    }
    
    public TreeSet(Comparator<? super E> comparator) {
        m = new TreeMap<>(comparator); // 自定义排序
    }
    
    // 添加元素:实际是向TreeMap添加键值对
    public boolean add(E e) {
        return m.put(e, PRESENT) == null;
    }
}

3.2 TreeSet的排序机制

java 复制代码
import java.util.*;

public class TreeSetSortingDemo {
    public static void main(String[] args) {
        System.out.println("=== TreeSet的排序机制 ===");
        
        // 1. 自然排序(元素必须实现Comparable接口)
        TreeSet<Integer> naturalSet = new TreeSet<>();
        naturalSet.add(5);
        naturalSet.add(2);
        naturalSet.add(8);
        naturalSet.add(1);
        System.out.println("自然排序: " + naturalSet); // [1, 2, 5, 8]
        
        // 2. 自定义排序(通过Comparator)
        TreeSet<String> customSet = new TreeSet<>(
            (s1, s2) -> s2.compareTo(s1) // 降序
        );
        customSet.add("Apple");
        customSet.add("Banana");
        customSet.add("Cherry");
        System.out.println("自定义排序(降序): " + customSet);
        
        // 3. 对象排序
        class Student implements Comparable<Student> {
            String name;
            int score;
            
            Student(String name, int score) {
                this.name = name;
                this.score = score;
            }
            
            @Override
            public int compareTo(Student other) {
                // 先按分数降序,再按姓名升序
                if (this.score != other.score) {
                    return Integer.compare(other.score, this.score);
                }
                return this.name.compareTo(other.name);
            }
            
            @Override
            public String toString() {
                return name + ":" + score;
            }
        }
        
        TreeSet<Student> studentSet = new TreeSet<>();
        studentSet.add(new Student("Alice", 85));
        studentSet.add(new Student("Bob", 92));
        studentSet.add(new Student("Charlie", 85));
        studentSet.add(new Student("David", 78));
        
        System.out.println("\n学生排序:");
        for (Student s : studentSet) {
            System.out.println(s);
        }
        
        // 4. TreeSet的特殊方法
        System.out.println("\n=== TreeSet的特殊方法 ===");
        TreeSet<Integer> numbers = new TreeSet<>();
        for (int i = 1; i <= 10; i++) {
            numbers.add(i);
        }
        
        System.out.println("原始集合: " + numbers);
        System.out.println("第一个元素: " + numbers.first()); // 1
        System.out.println("最后一个元素: " + numbers.last());  // 10
        System.out.println("小于等于5的最大元素: " + numbers.floor(5)); // 5
        System.out.println("大于等于5的最小元素: " + numbers.ceiling(5)); // 5
        System.out.println("小于5的元素: " + numbers.headSet(5)); // [1, 2, 3, 4]
        System.out.println("大于等于5的元素: " + numbers.tailSet(5)); // [5, 6, 7, 8, 9, 10]
        System.out.println("子集[3, 7): " + numbers.subSet(3, 7)); // [3, 4, 5, 6]
        
        // 5. 降序视图
        System.out.println("\n降序视图: " + numbers.descendingSet());
        
        // 6. 迭代器操作
        Iterator<Integer> it = numbers.descendingIterator();
        System.out.print("降序遍历: ");
        while (it.hasNext()) {
            System.out.print(it.next() + " ");
        }
        System.out.println();
    }
}

3.3 TreeSet的性能分析

java 复制代码
import java.util.*;

public class TreeSetPerformance {
    public static void main(String[] args) {
        System.out.println("=== TreeSet性能分析 ===");
        
        int size = 100000;
        
        // TreeSet性能测试
        TreeSet<Integer> treeSet = new TreeSet<>();
        
        // 1. 添加性能
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            treeSet.add(i);
        }
        long treeSetAddTime = System.currentTimeMillis() - start;
        
        // 2. 查找性能
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            treeSet.contains((int) (Math.random() * size));
        }
        long treeSetSearchTime = System.currentTimeMillis() - start;
        
        // 3. 范围查询性能
        start = System.currentTimeMillis();
        treeSet.subSet(size/4, size*3/4);
        long treeSetRangeTime = System.currentTimeMillis() - start;
        
        // 与HashSet对比
        HashSet<Integer> hashSet = new HashSet<>(size);
        
        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            hashSet.add(i);
        }
        long hashSetAddTime = System.currentTimeMillis() - start;
        
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            hashSet.contains((int) (Math.random() * size));
        }
        long hashSetSearchTime = System.currentTimeMillis() - start;
        
        System.out.println("\n性能对比:");
        System.out.println("==========================================");
        System.out.println("操作\t\t\tTreeSet\t\tHashSet");
        System.out.println("==========================================");
        System.out.printf("添加%d个元素\t\t%d ms\t\t%d ms%n", 
                         size, treeSetAddTime, hashSetAddTime);
        System.out.printf("查找10000次\t\t%d ms\t\t%d ms%n",
                         treeSetSearchTime, hashSetSearchTime);
        System.out.printf("范围查询\t\t\t%d ms\t\t不支持%n", treeSetRangeTime);
        
        // TreeSet的时间复杂度
        System.out.println("\n时间复杂度对比:");
        System.out.println("操作\t\t\tTreeSet\t\tHashSet");
        System.out.println("添加\t\t\tO(log n)\tO(1)");
        System.out.println("查找\t\t\tO(log n)\tO(1)");
        System.out.println("删除\t\t\tO(log n)\tO(1)");
        System.out.println("遍历\t\t\tO(n)\t\tO(n)");
        System.out.println("范围查询\t\t\tO(log n + k)\t不支持");
    }
}

四、LinkedHashSet:保持插入顺序的HashSet

4.1 LinkedHashSet的内部结构

java 复制代码
// LinkedHashSet的简化内部结构示意
public class LinkedHashSet<E> extends HashSet<E> {
    // 核心:继承自HashSet,但使用LinkedHashMap作为底层实现
    
    // 构造函数
    public LinkedHashSet() {
        super(16, 0.75f, true); // 调用HashSet的特定构造函数
    }
    
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, 0.75f, true);
    }
    
    // LinkedHashSet在迭代时保持插入顺序
}

4.2 LinkedHashSet的特点与使用

java 复制代码
import java.util.*;

public class LinkedHashSetDemo {
    public static void main(String[] args) {
        System.out.println("=== LinkedHashSet特点演示 ===");
        
        // 1. 保持插入顺序
        LinkedHashSet<String> linkedSet = new LinkedHashSet<>();
        linkedSet.add("First");
        linkedSet.add("Second");
        linkedSet.add("Third");
        linkedSet.add("First"); // 重复,不会添加
        
        System.out.println("LinkedHashSet遍历(保持插入顺序):");
        for (String s : linkedSet) {
            System.out.println(s); // 输出顺序与添加顺序相同
        }
        
        // 2. 访问顺序的影响(LRU)
        System.out.println("\n=== 访问顺序LRU演示 ===");
        
        // 创建一个按访问顺序排序的LinkedHashSet
        LinkedHashSet<String> lruCache = new LinkedHashSet<>(16, 0.75f) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > 3; // 最大容量为3
            }
        };
        
        // 转换为LinkedHashMap来模拟LRU缓存
        LinkedHashMap<String, String> lruMap = new LinkedHashMap<>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > 3;
            }
        };
        
        lruMap.put("A", "ValueA");
        lruMap.put("B", "ValueB");
        lruMap.put("C", "ValueC");
        
        System.out.println("初始状态: " + lruMap.keySet());
        
        // 访问A,使其变为最近访问的
        lruMap.get("A");
        System.out.println("访问A后: " + lruMap.keySet());
        
        // 添加新元素D,会淘汰最久未访问的B
        lruMap.put("D", "ValueD");
        System.out.println("添加D后: " + lruMap.keySet());
        
        // 3. 性能对比
        System.out.println("\n=== 性能特点 ===");
        System.out.println("LinkedHashSet在HashSet的基础上:");
        System.out.println("- 优点:保持插入顺序,迭代性能更好");
        System.out.println("- 缺点:需要额外空间存储链表,插入稍慢");
        System.out.println("- 时间复杂度:与HashSet相同,都是O(1)");
    }
}

五、三大Set实现类的全方位对比

|-----------------|---------|---------------|-----------|
| 特性 | HashSet | LinkedHashSet | TreeSet |
| 底层实现 | HashMap | LinkedHashMap | TreeMap |
| 数据结构 | 哈希表 | 哈希表+链表 | 红黑树 |
| 排序保证 | 无 | 插入顺序 | 自然/自定义 |
| 允许null元素 | 是 | 是 | 否 |
| 时间复杂度(添加/查找/删除) | O(1) | O(1) | O(log n) |
| 内存占用 | 较低 | 中等 | 较高 |
| 线程安全 | 否 | 否 | 否 |
| 迭代性能 | 一般 | 优秀(顺序迭代) | 优秀(有序迭代) |
| 特殊功能 | 无 | 保持顺序 | 范围查询、排序操作 |

六、Set选择指南与最佳实践

选择决策流程图

复制代码
开始选择Set实现
    │
    ├─ 是否需要元素有序?
    │   ├─ 是 → 选择TreeSet
    │   │       ├─ 需要自然排序? → 元素实现Comparable接口
    │   │       └─ 需要自定义排序? → 提供Comparator
    │   └─ 否 → 
    │        ├─ 是否需要保持插入顺序?
    │        │   ├─ 是 → 选择LinkedHashSet
    │        │   └─ 否 → 选择HashSet
    │        └─ 是否需要最高性能?
    │            ├─ 是 → 选择HashSet
    │            └─ 否 → 根据其他需求选择
    └─ 是否需要null元素?
        ├─ 是 → 避免使用TreeSet(除非自定义Comparator支持null)
        └─ 否 → 所有实现都可用

七、总结与建议

7.1 核心要点回顾

  1. HashSet

    • 基于HashMap,哈希表实现

    • 无序,性能最优(O(1))

    • 需要正确实现hashCode和equals

  2. TreeSet

    • 基于TreeMap,红黑树实现

    • 有序(自然排序或自定义Comparator)

    • 性能O(log n),支持范围查询

  3. LinkedHashSet

    • 基于LinkedHashMap

    • 保持插入顺序

    • 性能接近HashSet,内存占用略高

7.2 选择决策表

需求 推荐实现 原因
快速查找,无需顺序 HashSet 性能最优
保持插入顺序 LinkedHashSet 有序且性能好
自然排序 TreeSet 自动排序
自定义排序 TreeSet + Comparator 灵活排序
枚举类型 EnumSet 性能极致优化
线程安全,读多写少 CopyOnWriteArraySet 并发安全
线程安全,读写均衡 Collections.synchronizedSet 通用方案

7.3 性能调优建议

  1. HashSet/LinkedHashSet

    • 预估大小,设置初始容量避免扩容

    • 合理设置负载因子(默认0.75)

    • 确保hashCode分布均匀

  2. TreeSet

    • 对于自定义对象,提供高效的Comparator

    • 避免频繁的结构修改

  3. 通用建议

    • 小数据集(<1000):差异不大,按需求选择

    • 大数据集:考虑内存和性能平衡

    • 频繁操作:根据操作类型选择最优实现

7.4 常见问题解答

Q1:为什么HashSet允许null而TreeSet不允许?

A1:HashSet基于equals/hashCode,null可以参与比较;TreeSet基于比较器,null无法参与比较。

Q2:如何选择HashSet的初始容量?

A2:预估最终大小 ÷ 负载因子(默认0.75)。例如预计有1000个元素,设置初始容量为1334(1000/0.75)。

Q3:TreeSet和排序的List哪个更好?

A3:TreeSet自动维护排序,但元素唯一;List可以重复,但需要手动排序。根据是否需要去重和排序频率选择。


下篇预告:《Java集合(四):Map接口详解与HashMap、TreeMap、LinkedHashMap比较》

通过本篇学习,你应该已经掌握了Set接口的三大核心实现及其应用场景。在实际开发中,合理选择Set实现是保证程序性能的关键。记住:没有最好的数据结构,只有最适合的场景!

相关推荐
野生的码农2 小时前
码农的妇产科实习记录
android·java·人工智能
毕设源码-赖学姐3 小时前
【开题答辩全过程】以 高校人才培养方案管理系统的设计与实现为例,包含答辩的问题和答案
java
一起努力啊~3 小时前
算法刷题-二分查找
java·数据结构·算法
小途软件3 小时前
高校宿舍访客预约管理平台开发
java·人工智能·pytorch·python·深度学习·语言模型
J_liaty3 小时前
Java版本演进:从JDK 8到JDK 21的特性革命与对比分析
java·开发语言·jdk
+VX:Fegn08954 小时前
计算机毕业设计|基于springboot + vue律师咨询系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
daidaidaiyu4 小时前
一文学习和实践 当下互联网安全的基石 - TLS 和 SSL
java·netty
hssfscv4 小时前
Javaweb学习笔记——后端实战2_部门管理
java·笔记·学习
NE_STOP4 小时前
认识shiro
java
kong79069284 小时前
Java基础-Lambda表达式、Java链式编程
java·开发语言·lambda表达式