CodeGym /课程 /JAVA 25 SELF /Set:HashSet 与 TreeSet,元素唯一性

Set:HashSet 与 TreeSet,元素唯一性

JAVA 25 SELF
第 26 级 , 课程 3
可用

1. 引言

先从一个生活中的例子开始。想象你要办一场派对,需要拟定一份来宾名单。你发出邀请后发现,同一个人被列了两次(甚至三次——谁让他这么爱参加派对!)。如果你使用的是普通列表(List),就很容易出现这样的重复。但如果你有一个能自动阻止同一个来宾被添加两次的集合——生活就简单多了。

这时就轮到 Set(集合、集合论意义上的“集合”)登场了。

Set 是一种只存储唯一元素的集合。如果尝试添加已经存在的元素,它不会被添加(也不会伤害任何人感情)。

Set 接口:基本特性

在 Java 中,Set 是一个定义“无重复”集合行为的接口。它继承自 Collection 接口,因此支持添加(add)、删除(remove)、检查元素是否存在(contains)以及遍历等操作。

关键特点:

  • Set 中不可能有两个相同的元素。
  • 元素的存储顺序可能是任意的——取决于具体实现。
  • 没有索引:无法像列表那样通过下标访问元素。

声明语法

Set<String> guests = new HashSet<>();

2. HashSet:快速、简单、无顺序

HashSetSet 接口最常用的实现。它基于哈希表(类似 HashMap,但没有“键–值”对,只有唯一的值)。它的最大优点是:增、删、查都很快

HashSet 如何工作?

可以把它想象成一个箱子。为了快速判断箱子里是否已经有“类似”的东西,每件物品都会得到一个“编号”——哈希码。当你向 HashSet 中添加元素时,首先会计算该元素的哈希码。如果之前没出现过,就把元素放进集合;如果这个哈希已经存在,就会再通过 equals() 做一次相等性检查。只有在对象确实相同的情况下,新的元素才不会被添加。

也就是说,HashSet 会自动保证唯一性:两个相同的对象不会同时存在。

有个有趣的点:如果你使用自己的类并想把它们放进 HashSet,就需要重写 equals()hashCode() 方法。否则集合可能表现得不可预期——看起来相同的对象会被视为不同。

HashSet 的主要方法

Set<String> guests = new HashSet<>();

guests.add("Ivan");
guests.add("Maria");
guests.add("Peter");
guests.add("Ivan"); // 重复项!不会被添加。

System.out.println(guests); // [Ivan, Maria, Peter] — 顺序可能是任意的

guests.remove("Peter"); // 删除元素
System.out.println(guests.contains("Maria")); // true
System.out.println(guests.size()); // 2

我们来用代码试试

假设在我们的应用中,希望保存唯一的任务名称,避免出现两个同名任务:

import java.util.HashSet;
import java.util.Set;

public class UniqueTasksDemo {
    public static void main(String[] args) {
        Set<String> tasks = new HashSet<>();
        tasks.add("完成 Java 作业");
        tasks.add("撸猫");
        tasks.add("完成 Java 作业"); // 重复项!

        System.out.println("任务列表:");
        for (String task : tasks) {
            System.out.println("- " + task);
        }
        // 列表中只有两个任务,重复项不会被添加
    }
}

3. TreeSet:顺序很重要!

有时我们不仅需要唯一性,还需要有序的元素集合。比如,希望看到来宾名字按字母顺序排列,而不是随机顺序。这时可以使用 TreeSet

TreeSetSet 的一种实现,它按自然顺序(升序)存储元素。它基于“红黑树”结构。

TreeSet 使用示例

import java.util.Set;
import java.util.TreeSet;

public class SortedGuestsDemo {
    public static void main(String[] args) {
        Set<String> guests = new TreeSet<>();
        guests.add("Wolly");
        guests.add("Alex");
        guests.add("Kate");
        guests.add("Alex"); // 重复项!

        System.out.println("来宾(按字母顺序):");
        for (String guest : guests) {
            System.out.println("- " + guest);
        }
        // 输出:
        // - Alex
        // - Wolly
        // - Kate
    }
}

请注意:如果你添加了重复项,它不会出现在集合中。一切都如预期!

什么时候使用 TreeSet?

  • 当需要有序的唯一元素集合时。
  • 当需要快速查找,但对插入速度不那么敏感时(比 HashSet 略慢)。
  • 如果元素是你自定义的类,则它们需要是“可比较的”(实现 Comparable 接口),或者你需要提供自己的 Comparator

4. 实用要点

HashSet vs TreeSet:该如何选择?

指标 HashSet TreeSet
存储顺序 不保证 按升序排序
操作速度 更快 (O(1)) 较慢 (O(log n))
类型要求 任意(只需 equals()/hashCode() ComparableComparator
典型场景 需要对唯一元素进行快速访问时 需要有序输出/遍历时

使用 Set 的一些特点

  • 没有索引。List 不同,Set 没有 get(int index) 方法。如果需要按下标访问——请使用 List
  • 没有重复。 如果你尝试添加已存在的元素,它不会被添加。方法 add 将返回 false
  • 不保证顺序(TreeSet 除外)。HashSet 中,元素顺序每次运行都可能不同。如果需要保持插入顺序,请使用 LinkedHashSet
  • null 值。
    • HashSet 允许存储一个 null 元素。
    • TreeSet 在没有提供专用 Comparator 的情况下不允许添加 null,否则会抛出 NullPointerException

5. Set 的常见任务

从列表中去重

假设我们有一个学生名单,其中有些名字出现了两次。需要只保留唯一的名字:

import java.util.*;

public class RemoveDuplicatesDemo {
    public static void main(String[] args) {
        List<String> students = Arrays.asList("安娜", "Ingvar", "安娜", "Maria", "Ingvar", "帕维尔");

        Set<String> uniqueStudents = new HashSet<>(students);

        System.out.println("唯一的学生: " + uniqueStudents);
        // 顺序不保证!
    }
}

如果需要有序的结果——请使用 TreeSet

Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("唯一的学生(按字母顺序): " + sortedUniqueStudents);

检查唯一性(例如,用户登录名)

Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");

String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
    System.out.println("该登录名已被占用!");
} else {
    System.out.println("登录名可用!");
}

遍历集合中的元素

可以使用 for-each 循环进行遍历:

for (String name : uniqueStudents) {
    System.out.println(name);
}

6. 使用 Set 的常见错误

错误 1:期望 HashSet 有确定的元素顺序。 很多初学者会惊讶:为什么集合中的元素以“奇怪”的顺序输出。这是正常的——HashSet 不保证顺序。如果需要保持插入顺序——使用 LinkedHashSet;如果需要排序——使用 TreeSet

错误 2:尝试通过索引访问元素。 有时会写出类似 set.get(0) 的代码。这样是不行的:Set 不支持下标访问。需要下标访问——请使用 List

错误 3:存放可变对象。 如果你存放的对象中,用于 equals()/hashCode() 的字段是可变的,那么在修改这些字段后,该元素可能会对集合“不可见”。请让元素保持不可变,或不要修改其标识性字段。

错误 4:期望重复元素会被添加。 同一个元素添加多次不会增加集合大小——重复项会被忽略,方法 add 将返回 false

错误 5:使用基本类型。 诸如 Set<int> 之类的写法无法通过编译。请使用包装类型:Set<Integer>Set<Double> 等等。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION