Java Set 教程

更新于 2025-12-26

Jakob Jenkov 2020-10-13

Java 的 Set 接口(java.util.Set)表示一个对象的集合,其中每个对象在该 Set 中都是唯一的。换句话说,同一个对象不能在 Java Set 中出现多次。

Java Set 接口是一个标准的 Java 接口,并且是 Java Collection 接口的子类型,也就是说 Set 继承自 Collection

你可以向 Java Set 中添加任意 Java 对象。如果该 Set 没有使用 Java 泛型(Generics) 进行类型限定,那么你甚至可以在同一个 Set 中混合不同类型的对象(类)。不过,在实际开发中,这种混用不同类型的对象的做法并不常见。

Java Set 与 List 的区别

Java SetJava List 接口非常相似,它们都表示一个元素的集合。但两者之间存在一些显著的区别,这些区别也反映在它们各自接口的方法上。

  • 第一点区别:Java Set 中不能包含重复元素;而 Java List 允许同一个元素多次出现。
  • 第二点区别Set 中的元素没有保证的内部顺序;而 List 中的元素具有明确的内部顺序,并且可以按照该顺序进行遍历。

Java Set 示例

下面是一个简单的 Java Set 示例,让你对 Set 的基本用法有个初步了解:

package com.jenkov.collections;

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

public class SetExample {
    public static void main(String[] args) {
        Set<String> setA = new HashSet<>();
        setA.add("element");
        System.out.println(setA.contains("element"));
    }
}

这个示例创建了一个 HashSet(它是 Java API 中实现 Set 接口的类之一),然后向其中添加了一个字符串对象,最后检查该 Set 是否包含刚刚添加的元素。

Set 的实现类

由于 SetCollection 的子类型,因此 Collection 接口中的所有方法在 Set 接口中也都可用。

因为 Set 是一个接口,所以你需要实例化一个具体的实现类才能使用它。Java Collections API 提供了以下几种 Set 实现:

  • java.util.EnumSet
  • java.util.HashSet
  • java.util.LinkedHashSet
  • java.util.TreeSet

这些 Set 实现在迭代时的元素顺序以及插入和访问元素的时间复杂度(大 O 表示法)方面略有不同:

  • HashSet:底层基于 HashMap 实现。不保证迭代时元素的顺序。
  • LinkedHashSet:与 HashSet 不同的是,它保证迭代顺序与插入顺序一致。如果重新插入一个已存在的元素,不会改变其原有顺序。
  • TreeSet:也保证迭代顺序,但顺序是元素的自然排序顺序(如果元素实现了 Comparable 接口),或者由指定的 Comparator 决定。

此外,java.util.concurrent 包中也提供了一些并发安全的 Set 实现,但本教程暂不涉及并发工具相关内容。

创建 Set

以下是几种创建 Set 实例的方式:

package com.jenkov.collections;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

public class SetExample {
    public static void main(String[] args) {
        Set<String> setA = new HashSet<>();
        Set<String> setB = new LinkedHashSet<>();
        Set<String> setC = new TreeSet<>();
    }
}

泛型 Set

默认情况下,你可以向 Set 中放入任何 Object 类型的对象。但从 Java 5 开始,借助 Java 泛型(Generics),你可以限制 Set 中允许插入的对象类型。

例如:

Set<MyObject> set = new HashSet<MyObject>();

这个 Set 现在只能插入 MyObject 类型的实例。在访问或遍历时,也不需要进行强制类型转换:

for (MyObject anObject : set) {
    // 对 anObject 执行操作...
}

通常建议在明确知道集合元素类型时,始终为 Java Set 指定泛型类型。本教程中的大多数示例都使用了泛型。

更多关于 Java 泛型的信息,请参阅 Java 泛型教程

Set.of()(Java 9+)

从 Java 9 开始,Set 接口提供了一组静态工厂方法,用于创建**不可修改(immutable)**的 Set 实例。

这些静态方法名为 of(),可接受零个或多个参数。

创建空的不可变 Set:

Set<String> set = Set.of(); // 指定泛型
// 或
Set set = Set.of(); // 未指定泛型(不推荐)

创建包含元素的不可变 Set:

Set<String> set = Set.of("val1", "val2", "val3");

注意:通过 Set.of() 创建的 Set 是不可变的,任何试图修改它的操作(如 add()remove())都会抛出 UnsupportedOperationException

向 Set 中添加元素

要向 Set 中添加元素,调用其 add() 方法(该方法继承自 Collection 接口):

Set<String> setA = new HashSet<>();
setA.add("element 1");
setA.add("element 2");
setA.add("element 3");

这三次 add() 调用将三个 String 实例添加到 Set 中。

遍历 Set 中的元素

有两种主要方式可以遍历 Java Set 中的元素:

  1. 使用从 Set 获取的 Iterator
  2. 使用 for-each 循环

此外,还可以使用 Java Stream API(见下文)。

注意:遍历时元素的顺序取决于所使用的 Set 实现类。

使用 Iterator 遍历 Set

要使用 Java Iterator 遍历 Set,首先需要通过 iterator() 方法获取一个 Iterator

Set<String> setA = new HashSet<>();
setA.add("element 1");
setA.add("element 2");
setA.add("element 3");

Iterator<String> iterator = setA.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    // 处理 element
}

使用 for-each 循环遍历 Set

第二种方式是使用 for-each 循环:

Set<String> set = new HashSet<>();
for (String str : set) {
    System.out.println(str);
}

由于 Set 接口实现了 Java Iterable 接口,因此支持 for-each 循环。

最佳实践:当 Set 指定了泛型类型时,在 for-each 循环中直接使用该泛型类型,避免强制类型转换。

使用 Java Stream API 遍历 Set

第三种方式是通过 Java Stream API

Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");

set.stream().forEach(element -> {
    System.out.println(element);
});

更多关于 Stream API 的用法,请参考我的 Java Stream API 教程

从 Set 中移除元素

通过调用 remove(Object o) 方法可以从 Set 中移除元素:

set.remove("object-to-remove");

注意:Set 不支持通过索引移除元素,因为 Set 本身没有索引概念,其元素顺序取决于具体实现。

清空 Set

使用 clear() 方法可以移除 Set 中的所有元素:

set.clear();

将另一个集合的所有元素添加到 Set 中

Set 接口提供了 addAll(Collection<? extends E> c) 方法,用于将另一个集合(ListSet)中的所有元素添加到当前 Set 中。这在集合论中对应于**并集(union)**操作。

Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");

Set<String> set2 = new HashSet<>();
set2.add("four");
set2.addAll(set); // set2 现在包含 "four", "one", "two", "three"

从 Set 中移除另一个集合中存在的所有元素

removeAll(Collection<?> c) 方法会移除当前 Set 中所有出现在指定集合中的元素。这在集合论中称为差集(difference)

Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");

Set<String> set2 = new HashSet<>();
set2.add("three");

set.removeAll(set2); // set 现在只包含 "one" 和 "two"

仅保留 Set 中与另一个集合共有的元素

retainAll(Collection<?> c) 方法会保留当前 Set 中所有出现在指定集合中的元素,移除其余元素。这在集合论中称为交集(intersection)

Set<String> set = new HashSet<>();
set.add("one");
set.add("two");
set.add("three");

Set<String> set2 = new HashSet<>();
set2.add("three");
set2.add("four");

set.retainAll(set2); // set 现在只包含 "three"

获取 Set 的大小

使用 size() 方法可以获取 Set 中元素的数量:

Set<String> set = new HashSet<>();
set.add("123");
set.add("456");
set.add("789");

int size = set.size(); // size = 3

判断 Set 是否为空

使用 isEmpty() 方法可以判断 Set 是否不包含任何元素:

Set<String> set = new HashSet<>();
boolean isEmpty = set.isEmpty(); // true

也可以通过比较 size() 是否为 0 来判断:

boolean isEmpty = (set.size() == 0);

判断 Set 是否包含某个元素

使用 contains(Object o) 方法可以检查 Set 是否包含指定元素:

Set<String> set = new HashSet<>();
set.add("123");
set.add("456");

boolean contains123 = set.contains("123"); // true

Set 内部会遍历其元素,并使用元素的 equals() 方法 与传入的对象进行比较。

注意Set 允许包含 null 值,因此也可以检查是否包含 null

set.add(null);
boolean containsNull = set.contains(null); // true

当参数为 null 时,contains() 方法不会调用 equals(),而是使用 == 运算符进行比较。

将 Java Set 转换为 List

可以通过创建一个 List 并调用其 addAll() 方法(传入 Set 作为参数)来实现转换:

Set<String> set = new HashSet<>();
set.add("123");
set.add("456");

List<String> list = new ArrayList<>();
list.addAll(set); // list 现在包含 "123" 和 "456"

更多细节请查阅 JavaDoc

Set 接口还有更多功能,本文仅聚焦于最常见的操作:添加/删除元素遍历元素

欲了解完整功能,请查阅官方 JavaDoc