【Day14】集合框架(二):Set 接口(HashSet、TreeSet)去重与排序

哈喽,各位 Java 学习者!欢迎来到《Java 学习日记》的第十四篇内容~ 上一篇我们掌握了有序可重复的 List 接口,今天要学习它的 "互补型" 集合 ------Set 接口,以及它的两个核心实现类:HashSet(哈希去重)和TreeSet(排序去重)。Set 作为 "无序、不可重复" 的集合,是开发中实现 "去重" 和 "有序去重" 的核心工具。本文会从 Set 的核心特性、HashSet 的哈希去重原理、TreeSet 的排序机制,到实战场景选择,帮你彻底掌握 Set 的使用!

一、Set 接口:无序、不可重复的集合核心

1. 为什么需要 Set?

List 能存储重复元素,但开发中经常需要 "去重" 场景(如统计用户访问的唯一 IP、筛选不重复的商品 ID),Set 的核心价值就是保证元素唯一性,同时不维护插入顺序(HashSet)或按指定规则排序(TreeSet)。

2. Set 接口的核心特性

特性 说明
不可重复性 不允许存储重复元素(判断标准:equals()返回 true 且hashCode()值相等)
无序性 (HashSet)元素不按插入顺序排列,底层由哈希表决定;(TreeSet)按指定规则排序
无索引 不支持索引访问(无get(int index)方法),只能遍历
允许 null (HashSet)允许存储一个 null;(TreeSet)不允许 null(排序时会空指针)

3. Set 接口的常用方法(必背)

Set 继承自Collection接口,无新增方法,核心方法如下(与 List 相比,无索引相关方法):

方法 作用 示例
add(E e) 添加元素(重复则返回 false,不添加) set.add("Java")
remove(Object o) 删除指定元素(存在则返回 true) set.remove("Java")
contains(Object o) 判断是否包含指定元素 set.contains("Java")
size() 获取元素个数 set.size()
isEmpty() 判断是否为空 set.isEmpty()
clear() 清空所有元素 set.clear()
iterator() 获取迭代器,用于遍历 Iterator<String> it = set.iterator()

实战 1:Set 接口基础用法(去重)

java

运行

java 复制代码
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class SetBasicDemo {
    public static void main(String[] args) {
        // 1. 创建Set对象(多态:父接口引用指向子类实现)
        Set<String> set = new HashSet<>();

        // 2. 添加元素(自动去重)
        set.add("Java");
        set.add("Python");
        set.add("Java"); // 重复元素,添加失败
        set.add(null); // HashSet允许一个null
        System.out.println("添加重复元素后大小:" + set.size()); // 3(Java、Python、null)

        // 3. 遍历元素(三种方式)
        // 方式1:增强for循环(唯一推荐的遍历方式)
        System.out.println("\n增强for循环遍历:");
        for (String s : set) {
            System.out.println(s); // 输出顺序不确定(无序)
        }

        // 方式2:迭代器遍历(支持删除元素)
        System.out.println("\n迭代器遍历(删除null):");
        Iterator<String> it = set.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if (s == null) {
                it.remove(); // 安全删除
            } else {
                System.out.println(s);
            }
        }

        // 方式3:forEach Lambda(Java 8+)
        System.out.println("\nforEach遍历:");
        set.forEach(s -> System.out.println(s));

        // 4. 其他常用方法
        System.out.println("\n是否包含Python:" + set.contains("Python")); // true
        set.remove("Python");
        System.out.println("删除Python后大小:" + set.size()); // 1
    }
}

二、HashSet:基于哈希表的去重集合(开发首选)

1. 底层原理

HashSet是 Set 接口的哈希表实现 ,底层依赖HashMap(Java 8+),核心逻辑是:

  • HashSet 内部维护一个 HashMap,将元素作为 HashMap 的key存储(value 固定为一个空对象);
  • 利用 HashMap 的 key 唯一性实现 Set 的去重;
  • 哈希表底层是 "数组 + 链表 + 红黑树"(Java 8+),保证添加 / 查询 / 删除的高效性。

核心源码简化如下:

java

运行

java 复制代码
public class HashSet<E> extends AbstractSet<E> implements Set<E> {
    private transient HashMap<E, Object> map;
    // 固定value,节省内存
    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>(); // 底层创建HashMap
    }

    // 添加元素:本质是向HashMap中put(key, PRESENT)
    public boolean add(E e) {
        return map.put(e, PRESENT) == null;
    }

    // 删除元素:本质是删除HashMap的key
    public boolean remove(Object o) {
        return map.remove(o) == PRESENT;
    }
}

2. 去重核心规则(面试高频)

HashSet 判断元素是否重复的两步规则

  1. 先比较哈希值hashCode()方法):如果哈希值不同,直接判定为不同元素;
  2. 若哈希值相同,再比较内容equals()方法):
    • equals()返回 true → 重复元素,不添加;
    • equals()返回 false → 哈希冲突,存储到链表 / 红黑树中。

实战 2:自定义对象去重(必须重写 hashCode 和 equals)

java

运行

java 复制代码
import java.util.HashSet;
import java.util.Set;

// 学生类:需重写hashCode和equals才能实现去重
class Student {
    private String id; // 学号(唯一标识)
    private String name;
    private int age;

    public Student(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    // 重写equals:按学号判断是否为同一个学生
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return id.equals(student.id); // 仅学号相同即为同一学生
    }

    // 重写hashCode:基于学号生成哈希值(保证equals相等则hashCode相等)
    @Override
    public int hashCode() {
        return id.hashCode();
    }

    @Override
    public String toString() {
        return "Student{id='" + id + "', name='" + name + "', age=" + age + "}";
    }
}

public class HashSetCustomObjDemo {
    public static void main(String[] args) {
        Set<Student> studentSet = new HashSet<>();

        // 添加两个学号相同的学生(应视为重复)
        studentSet.add(new Student("001", "张三", 18));
        studentSet.add(new Student("001", "张三", 19)); // 重复,添加失败
        studentSet.add(new Student("002", "李四", 20));

        // 输出:仅2个元素(去重成功)
        System.out.println("Set大小:" + studentSet.size()); // 2
        studentSet.forEach(System.out::println);
    }
}

3. 核心特性与性能分析

特性 说明
底层结构 HashMap(数组 + 链表 + 红黑树)
去重规则 hashCode() + equals()
顺序 无序(不保证插入顺序,也不保证排序)
允许 null 是(仅一个)
线程安全 非线程安全(多线程需用Collections.synchronizedSet
时间复杂度 添加 / 查询 / 删除:O (1)(无哈希冲突),O (n)(极端哈希冲突)

实战 3:HashSet 性能测试(去重效率)

java

运行

java 复制代码
import java.util.HashSet;
import java.util.Set;

public class HashSetPerformanceDemo {
    public static void main(String[] args) {
        // 测试100万条数据去重
        Set<Integer> set = new HashSet<>();
        long start = System.currentTimeMillis();

        for (int i = 0; i < 1000000; i++) {
            set.add(i % 100000); // 模拟10倍重复数据
        }

        long end = System.currentTimeMillis();
        System.out.println("HashSet添加100万条重复数据耗时:" + (end - start) + "ms"); // 约20ms
        System.out.println("去重后元素个数:" + set.size()); // 100000
    }
}

三、TreeSet:基于红黑树的排序去重集合

1. 底层原理

TreeSet是 Set 接口的红黑树实现 ,底层依赖TreeMap,核心逻辑是:

  • TreeSet 内部维护一个 TreeMap,将元素作为 TreeMap 的 key 存储;
  • 利用红黑树的特性,自动对元素进行自然排序 (如数字升序、字符串字典序)或自定义排序
  • 排序的前提是元素实现Comparable接口,或创建 TreeSet 时指定Comparator比较器。

核心源码简化如下:

java

运行

java 复制代码
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E> {
    private transient NavigableMap<E, Object> m;
    private static final Object PRESENT = new Object();

    public TreeSet() {
        m = new TreeMap<>(); // 底层创建TreeMap
    }

    public TreeSet(Comparator<? super E> comparator) {
        m = new TreeMap<>(comparator); // 指定自定义比较器
    }

    public boolean add(E e) {
        return m.put(e, PRESENT) == null;
    }
}

2. 排序核心规则(面试高频)

TreeSet 的排序分为两种方式,优先级:自定义比较器 > 自然排序

方式 1:自然排序(元素实现 Comparable 接口)

  • 元素需实现Comparable接口,重写compareTo(T o)方法;
  • compareTo返回值规则:
    • 返回 0 → 元素重复,不添加;
    • 返回正数 → 当前元素大于参数元素;
    • 返回负数 → 当前元素小于参数元素。

方式 2:自定义比较器(Comparator)

  • 创建 TreeSet 时传入Comparator对象,重写compare(T o1, T o2)方法;
  • 无需修改元素类,灵活性更高。

3. 核心特性与性能分析

特性 说明
底层结构 TreeMap(红黑树)
去重规则 compareTo/compare 返回 0 即为重复
顺序 有序(自然排序 / 自定义排序)
允许 null 否(排序时会抛出 NullPointerException)
线程安全 非线程安全
时间复杂度 添加 / 查询 / 删除:O (log n)(红黑树的平衡特性)

实战 4:自然排序(实现 Comparable)

java

运行

java 复制代码
import java.util.Set;
import java.util.TreeSet;

// 商品类:实现Comparable,按价格升序排序
class Goods implements Comparable<Goods> {
    private String id;
    private String name;
    private double price;

    public Goods(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    // 自然排序:按价格升序,价格相同则按ID排序
    @Override
    public int compareTo(Goods o) {
        // 先按价格比较
        if (this.price > o.price) return 1;
        if (this.price < o.price) return -1;
        // 价格相同,按ID字典序
        return this.id.compareTo(o.id);
    }

    @Override
    public String toString() {
        return "Goods{id='" + id + "', name='" + name + "', price=" + price + "}";
    }
}

public class TreeSetNaturalSortDemo {
    public static void main(String[] args) {
        Set<Goods> goodsSet = new TreeSet<>();

        // 添加商品(自动按价格升序排序)
        goodsSet.add(new Goods("003", "手机", 2999.99));
        goodsSet.add(new Goods("001", "电脑", 5999.99));
        goodsSet.add(new Goods("002", "平板", 2999.99)); // 价格相同,按ID排序

        // 遍历:按价格升序输出
        System.out.println("按价格升序排序:");
        goodsSet.forEach(System.out::println);
    }
}

实战 5:自定义比较器(Comparator)

java

运行

java 复制代码
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public class TreeSetCustomComparatorDemo {
    public static void main(String[] args) {
        // 自定义比较器:按字符串长度降序,长度相同按字典序升序
        Set<String> set = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                // 先按长度降序
                int lenCompare = Integer.compare(s2.length(), s1.length());
                if (lenCompare != 0) {
                    return lenCompare;
                }
                // 长度相同,按字典序升序
                return s1.compareTo(s2);
            }
        });

        // 添加元素(自动按自定义规则排序)
        set.add("Java");
        set.add("Python");
        set.add("C");
        set.add("Go");
        set.add("JavaScript");

        // 遍历:按长度降序输出
        System.out.println("按字符串长度降序排序:");
        set.forEach(System.out::println);
    }
}

四、HashSet vs TreeSet:核心区别与场景选择

1. 核心区别对比表

特性 HashSet TreeSet
底层结构 哈希表(HashMap) 红黑树(TreeMap)
排序性 无序 有序(自然排序 / 自定义排序)
去重规则 hashCode() + equals() compareTo ()/compare () 返回 0
允许 null 是(仅一个)
性能 高(O (1)) 中(O (log n))
元素要求 可选重写 hashCode/equals(自定义对象) 必须实现 Comparable 或指定 Comparator
常用方法 基础 Collection 方法 新增排序相关方法(如 first ()、last ()、ceiling ())

2. 场景选择最佳实践

场景 推荐使用 原因
仅需去重,无需排序 HashSet 性能最高,实现简单
去重且需要排序(升序 / 降序 / 自定义规则) TreeSet 自动排序,无需手动处理
高频添加 / 查询 / 删除,无排序需求 HashSet O (1) 性能远超 TreeSet 的 O (log n)
需要获取最大 / 最小元素 TreeSet 提供 first ()、last () 等便捷方法
自定义对象去重 HashSet(重写 hashCode/equals) 比 TreeSet 更简单,性能更高

实战 6:场景对比(去重 + 排序 vs 仅去重)

java

运行

java 复制代码
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class SetScenarioDemo {
    public static void main(String[] args) {
        // 场景1:仅去重(用户访问的唯一IP)
        String[] ips = {"192.168.1.1", "10.0.0.1", "192.168.1.1", "172.16.0.1"};
        Set<String> ipSet = new HashSet<>();
        for (String ip : ips) {
            ipSet.add(ip);
        }
        System.out.println("唯一IP(无序):");
        ipSet.forEach(System.out::println);

        // 场景2:去重+排序(成绩排名)
        Integer[] scores = {85, 90, 78, 90, 88, 95};
        Set<Integer> scoreSet = new TreeSet<>();
        for (Integer score : scores) {
            scoreSet.add(score);
        }
        System.out.println("\n去重后成绩(升序):");
        scoreSet.forEach(System.out::println);
    }
}

五、Set 使用的高频坑点与优化技巧

1. 高频坑点

坑点 1:自定义对象去重失败(未重写 hashCode/equals)

  • 问题 :HashSet 存储自定义对象时,未重写hashCode()equals(),导致重复元素无法去重;
  • 解决 :必须同时重写hashCode()equals(),且保证 "equals 相等的对象,hashCode 一定相等"。

坑点 2:TreeSet 添加 null 元素(空指针异常)

  • 问题 :TreeSet 添加 null 会抛出NullPointerException
  • 解决:避免向 TreeSet 添加 null,或在比较器中处理 null。

坑点 3:TreeSet 排序规则冲突(compareTo 返回值错误)

  • 问题compareTo方法返回值逻辑错误,导致排序混乱或重复元素;
  • 解决 :严格遵循compareTo返回值规则(0 = 相等,正数 = 大于,负数 = 小于)。

2. 优化技巧

技巧 1:HashSet 初始化指定容量(减少扩容)

java

运行

java 复制代码
// 预估元素个数,指定初始容量和加载因子(默认0.75)
Set<String> set = new HashSet<>(100, 0.75f);

技巧 2:批量添加元素(addAll)

java

运行

java 复制代码
// 批量添加比循环单个add更快(减少哈希冲突检查次数)
Set<String> set = new HashSet<>();
Set<String> data = new HashSet<>();
data.add("A");
data.add("B");
set.addAll(data);

技巧 3:TreeSet 使用 Lambda 简化比较器(Java 8+)

java

运行

java 复制代码
// Lambda替代匿名内部类,代码更简洁
Set<String> set = new TreeSet<>((s1, s2) -> {
    // 按长度降序
    int lenCompare = Integer.compare(s2.length(), s1.length());
    return lenCompare != 0 ? lenCompare : s1.compareTo(s2);
});

六、综合实战:Set 实现投票系统(去重 + 排序)

java

运行

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

// 投票选项类
class VoteOption implements Comparable<VoteOption> {
    private String optionId; // 选项ID
    private String optionName; // 选项名称
    private int voteCount; // 票数

    public VoteOption(String optionId, String optionName) {
        this.optionId = optionId;
        this.optionName = optionName;
        this.voteCount = 0;
    }

    // 投票(票数+1)
    public void vote() {
        this.voteCount++;
    }

    // 自然排序:按票数降序,票数相同按ID升序
    @Override
    public int compareTo(VoteOption o) {
        int countCompare = Integer.compare(o.voteCount, this.voteCount);
        return countCompare != 0 ? countCompare : this.optionId.compareTo(o.optionId);
    }

    @Override
    public String toString() {
        return "选项:" + optionName + "(票数:" + voteCount + ")";
    }

    // getter
    public String getOptionId() { return optionId; }
}

// 投票系统
public class VoteSystem {
    // 存储已投票用户ID(去重,防止重复投票)
    private Set<String> votedUserIds = new HashSet<>();
    // 存储投票选项(排序,按票数降序)
    private Set<VoteOption> voteOptions = new TreeSet<>();

    // 初始化投票选项
    public VoteSystem() {
        voteOptions.add(new VoteOption("001", "Java"));
        voteOptions.add(new VoteOption("002", "Python"));
        voteOptions.add(new VoteOption("003", "C++"));
    }

    // 投票方法(去重+计数)
    public boolean vote(String userId, String optionId) {
        // 1. 检查是否重复投票
        if (votedUserIds.contains(userId)) {
            System.out.println("用户" + userId + "已投票,禁止重复投票!");
            return false;
        }

        // 2. 查找投票选项
        VoteOption targetOption = null;
        for (VoteOption option : voteOptions) {
            if (option.getOptionId().equals(optionId)) {
                targetOption = option;
                break;
            }
        }

        if (targetOption == null) {
            System.out.println("投票选项不存在!");
            return false;
        }

        // 3. 记录投票用户,更新票数
        votedUserIds.add(userId);
        targetOption.vote();
        System.out.println("用户" + userId + "投票成功!");
        return true;
    }

    // 展示投票结果(按票数降序)
    public void showVoteResult() {
        System.out.println("\n===== 投票结果(按票数降序) =====");
        voteOptions.forEach(System.out::println);
        System.out.println("总投票人数:" + votedUserIds.size());
    }

    public static void main(String[] args) {
        VoteSystem voteSystem = new VoteSystem();

        // 模拟投票(包含重复投票)
        voteSystem.vote("U001", "001"); // 成功
        voteSystem.vote("U002", "001"); // 成功
        voteSystem.vote("U001", "002"); // 重复投票,失败
        voteSystem.vote("U003", "002"); // 成功
        voteSystem.vote("U004", "003"); // 成功

        // 展示结果
        voteSystem.showVoteResult();
    }
}

总结

关键点回顾

  1. Set 核心特性:无序(HashSet)/ 有序(TreeSet)、不可重复,无索引,是去重的核心工具;
  2. HashSet :哈希表实现,去重规则是hashCode()+equals(),性能高,适合仅需去重的场景;
  3. TreeSet :红黑树实现,排序去重,规则是compareTo()/Comparator,适合需排序的去重场景;
  4. 核心坑点 :自定义对象去重需重写hashCode()equals(),TreeSet 不能添加 null;
  5. 场景选择:优先用 HashSet(性能高),需排序时用 TreeSet。

Set 是 Java 集合框架中与 List 互补的核心类型,掌握 HashSet 的去重原理和 TreeSet 的排序机制,能高效解决开发中的去重、排序问题。下一篇【Day15】,我们会讲解 "集合框架(三):Map 接口(HashMap、TreeMap)核心用法",帮你掌握键值对存储的核心容器!如果今天的内容对你有帮助,欢迎点赞 + 收藏 + 关注,有任何问题都可以在评论区留言,咱们一起讨论~ 明天见!🚀

相关推荐
weixin_515069662 小时前
BeanToMapUtil-对象转Map
java·工具类·java常用api
code_std2 小时前
保存文件到指定位置,读取/删除指定文件夹中文件
java·spring boot·后端
sort浅忆2 小时前
deeptest执行接口脚本,添加python脚本断言
开发语言·python
趣知岛2 小时前
JavaScript性能优化实战大纲
开发语言·javascript·性能优化
小许学java2 小时前
Spring事务和事务传播机制
java·数据库·spring·事务
大学生资源网2 小时前
基于Javaweb技术的宠物用品商城的设计与实现(源码+文档)
java·mysql·毕业设计·源码·springboot
汤姆yu2 小时前
基于springboot的热门文创内容推荐分享系统
java·spring boot·后端
星光一影2 小时前
教育培训机构消课管理系统智慧校园艺术舞蹈美术艺术培训班扣课时教务管理系统
java·spring boot·mysql·vue·mybatis·uniapp
im_AMBER2 小时前
weather-app开发手记 04 AntDesign组件库使用解析 | 项目设计困惑
开发语言·前端·javascript·笔记·学习·react.js