Java 集合:Set 集合

Java 的 Set 接口(java.util.Set)用于表示一组对象的集合,其核心特性是集合中的每个元素都是唯一的 —— 即同一个对象在 Set 中无法重复出现。

作为 Java 标准接口,Set 继承自 Collection 接口,是 Collection 体系中的重要子类型。

Set 可以存储任何 Java 对象。若未通过泛型指定元素类型,理论上能在同一个 Set 中混合存放不同类的对象,但这种用法在实际开发中并不常见。

  

Set 与 List 区别

Java 的 Set 接口与 List 接口虽同属元素集合的表示形式,但存在显著差异,这些差异在两者的方法设计中均有体现。主要区别如下:

(1)元素的重复性:Set 严格要求元素唯一,不允许同一元素重复出现;而 List 则允许元素重复,同一元素可多次存在于集合中。

(2)元素的顺序性:Set 对内部元素的存储顺序不做任何保证;而 List 中的元素具有明确的内部顺序,且能按照该顺序进行迭代访问。

  

Set 示例

下面是一个简单的 Set 示例,让你感受一下 Set 的工作方式:

package com.hxstrive.java_collection.set;

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

public class SetExample {

    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        set.add("element1");
        set.add("element2");
        set.add("element3");
        System.out.println( set.contains("element2") ); // true
        System.out.println( set.contains("element4") ); // false
    }
}

上面示例创建了一个 HashSet,然后,它向该集合中添加了三个字符串对象,最后检查该集合是否包含刚刚添加的元素。

  

Set 实现类

作为 Collection 接口的子类型,Set 接口完全继承了 Collection 接口中的所有方法,因此这些方法在 Set 中均具备可用性。

需要注意的是,Set 本身仅为一个接口,无法直接实例化使用。若要在代码中应用 Set 的特性,必须通过实例化其具体实现类来完成。在 Java 集合 API 中,常用的 Set 实现类主要包括以下几种,可根据实际需求选择使用:

  • java.util.EnumSet:专为枚举类型设计的 Set 实现,具有极高的性能和紧凑的内存占用。它的元素必须是同一枚举类型的实例,内部通过位向量实现,操作效率极高(如 add、remove、contains 等方法几乎是常量时间)。EnumSet 不允许包含 null 元素,且是有序的,元素顺序与枚举类中声明的顺序一致。它没有公共构造方法,通常通过静态方法(如 allOf()、of()、range() 等)创建实例。

  • java.util.HashSet:基于哈希表(HashMap)实现的 Set 接口,它不保证元素的顺序,且允许 null 元素(但最多只能有一个 null)。HashSet 的特点是添加、删除和查找元素的操作效率高(平均时间复杂度为 O(1)),但迭代顺序不确定。它通过元素的 hashCode() 和 equals() 方法来保证元素的唯一性,因此存储在 HashSet 中的对象需要正确重写这两个方法。

  • java.util.LinkedHashSet:继承自 HashSet,同样基于哈希表实现,但额外维护了一个双向链表来记录元素的插入顺序,因此迭代时会按照元素插入的顺序进行访问。它的性能略低于 HashSet(因为需要维护链表),但在迭代操作频繁的场景下表现更优。与 HashSet 一样,它允许 null 元素,且依赖 hashCode() 和 equals() 方法保证元素唯一性。

  • java.util.TreeSet:基于红黑树(一种自平衡的二叉搜索树)实现的 Set 接口,能够保证元素处于排序状态(自然排序或通过指定的 Comparator 排序)。TreeSet 不允许 null 元素(当使用自然排序时,元素需实现 Comparable 接口),其 add()、remove()、contains() 等操作的时间复杂度为 O (log n)。由于元素是有序的,它还提供了一些针对排序的特殊方法,如 subSet()、headSet()、tailSet() 等,用于获取指定范围的元素。

注意:

  • HashSet 由 HashMap 提供支持。在迭代元素时,它不保证元素的顺序。

  • LinkedHashSet 与 HashSet 的不同之处在于,它保证迭代期间元素的顺序与它们插入LinkedHashSet 时的顺序相同。重新插入已存在于 LinkedHashSet中 的元素不会改变这个顺序。

  • TreeSet 在迭代时也能保证元素的顺序,但该顺序是元素的排序顺序。换句话说,这个顺序就是如果你对包含这些元素的 List 或数组使用 Collections.sort() 方法时,元素应有的排序顺序。这种顺序要么由元素的自然顺序(如果它们实现了 Comparable 接口)决定,要么由特定的 Comparator 实现决定。

注意:java.util.concurrent 包中也有 Set 的实现,但在本教程中,不会涉及并发工具类。

  

Set 创建

以下是创建 Set 实例的几个示例:

package com.hxstrive.java_collection.list;

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) {
        // 创建 HashSet
        Set<String> setA = new HashSet<>();
        // 创建 LinkedHashSet
        Set<String> setB = new LinkedHashSet<>();
        // 创建 TreeSet
        Set<String> setC = new TreeSet<>();
    }
}

默认情况下,你可以将任何对象放入Set 中,例如:

Set setA = new HashSet();
setA.add("hello world");
setA.add(new Date());
setA.add(100.0f);

但从 Java5 开始,Java 泛型使得限制可以插入到 Set 中的对象类型成为可能。例如:

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

这个集合现在只能插入 MyObject 的实例。然后,你可以访问并迭代其元素,而无需对它们进行强制类型转换。如下所示:

for(MyObject anObject : set){
   //do someting to anObject...
}

  

Set.of() 方法

自自 Java 9 起,Set 接口新增了一组静态工厂方法,可用于创建不可修改(不可变)的 Set 实例。本节将介绍其中几种常用方法。

Java 集合:Set 集合

Java 为 Set 提供的静态工厂方法名为 of(),该方法可接受零个或多个参数。首先来看一个使用 Set.of() 创建空的不可变 Set 的示例:

Set set = Set.of();
set.add("hello"); // 抛出 java.lang.UnsupportedOperationException 异常

此示例创建了一个未指定泛型类型的不可修改的 Set。如果我们使用 add() 添加元素到该不可变的 Set,将会抛出如下异常:

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
	at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)

指定由 Set.of() 返回的 Set 的泛型类型方式如下所示:

Set<String> set3 = Set.<String>of();

你也可以创建包含自定义元素的不可修改 Set 实例,只需将这些元素作为参数传递给 of() 方法即可。下面是使用 Set.of() 创建包含元素的 Set 的示例:

Set<String> set3 = Set.<String>of("one", "two", "three");

  

Set 添加元素

要若要向 Set 集合中添加元素,可调用其 add() 方法(该方法继承自 Collection 接口)。以下为几个示例:

// 创建 HashSet 实例
Set<String> setA = new HashSet<>();
setA.add("element 1");
setA.add("element 2");
setA.add("element 3");
System.out.println("集合中的元素:" + setA);
//集合中的元素:[element 3, element 2, element 1]

上面代码进行了三次 add() 调用,向集合中添加了三个 String 实例。

Java 的 List 接口提供了 addAll() 方法,该方法能将另一个 Collection(无论是 List 还是 Set)中的所有元素添加到当前集合中。从集合论角度看,这相当于求当前集合与另一个 Collection 的并集。例如:

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);
System.out.println(set2); // [four, one, two, three]

执行此代码示例后,set2 将包含字符串元素 four,以及来自 set 的三个字符串元素 one、two 和 three。

  

Set 迭代元素

迭代 Set 的元素有两种方法:

  • 使用从集合(Set)中获取的迭代器(Iterator)。

  • 使用增强型 for 循环 for-each。

  • 使用 Stream API 迭代 Set。

使用迭代器迭代 Set

若要使用迭代器(Iterator)遍历 Set 集合中的元素,首先需要调用 Set 的 iterator() 方法从该集合中获取迭代器实例。例如:

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

Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
    System.out.println(iterator.next()); // 打印每个元素
}

使用增强型for循环迭代 Set

迭代 Set 元素的第二种方法是使用增强型 for 循环。例如:

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

for(String item : set) {
    System.out.println(item); // 打印每个元素
}

Set 接口实现了 Iterable 接口。这就是为什么你可以使用 for-each 循环来迭代 Set 中的元素。

使用 Stream API 迭代 Set

迭代 Set 的第三种方法是通过 Stream API。要使用 Stream API 迭代 Set,你必须从该 Set 创建一个 Stream。例如:

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

Stream<String> stream = set.stream();
stream.forEach(e -> {
    System.out.println(e); // 打印每个元素
});

  

Set 移除元素

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

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

System.out.println("移除前 " + set); //移除前 [element 3, element 2, element 1]
set.remove("element 1");
System.out.println("移除后 " + set); //移除后 [element 3, element 2]

注意,在 Set 中无法根据索引移除对象,因为元素的顺序取决于 Set 的实现。

如果要删除所有元素,你可以使用 clear() 方法。例如:

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

System.out.println("清除前 " + set); //清除前 [element 3, element 2, element 1]
set.clear();
System.out.println("清除后 " + set); //清除后 []

Set 接口还提供了 removeAll() 方法,该方法会移除当前 Set 中所有同时存在于另一个 Collection 中的元素。从集合论角度来说,这相当于求当前 Set 与另一个 Collection 的差集。例如:

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

Set<String> other = new HashSet<>();
other.add("element 3");
other.add("element 4");

System.out.println("删除前 " + set); //删除前 [element 3, element 2, element 1]
set.removeAll(other);
System.out.println("删除后 " + set); //删除后 [element 2, element 1]

运行上述示例后,集合将包含字符串元素“element 2”和“element 1”。元素“element 3”已被移除,因为它存在于 other 中,而 other 是作为参数传递给set.removeAll(other) 方法的。

  

Set 交集

Set 接口还提供了 retainAll() 方法,该方法会保留当前 Set 中与另一个 Collection 共有的所有元素,同时移除 Set 中不存在于另一个 Collection 里的元素。从集合论角度来看,这一操作相当于求当前 Set 与另一个 Collection 的交集。例如:

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

Set<String> other = new HashSet<>();
other.add("element 3");
other.add("element 4");

System.out.println("操作前 " + set); //操作前 [element 3, element 2, element 1]
set.retainAll(other);
System.out.println("操作后 " + set); //操作后 [element 3]

运行上述代码后,set 将只包含字符串元素“element 3”,这是同时存在于 set 和 other 集合中的唯一元素。

  

Set 大小

你可以使用 size() 方法获取 Set 的大小,即 Set 中包含的元素数量。例如:

Set<String> set = new HashSet<>();
set.add("element 1");
set.add("element 2");
set.add("element 3");
System.out.println("元素数量:" + set.size()); //元素数量:3

  

Set 是否为空

你可以通过调用 Set 的 isEmpty() 方法来检查集合是否为空(即是否不包含任何元素)。例如:

Set<String> set = new HashSet<>();
set.add("element 1");
set.add("element 2");
set.add("element 3");
System.out.println("是否为空:" + set.isEmpty()); //是否为空:false

你也可以通过将 size() 方法返回的值与 0 进行比较,来检查一个 Set 是否为空。例如:

Set<String> set = new HashSet<>();
set.add("element 1");
set.add("element 2");
set.add("element 3");
System.out.println("是否为空:" + (set.size() == 0)); //是否为空:false

因为 Set 的 size() 方法返回 0,因为该示例中的 Set 不包含任何元素。

  

Set 是否包含元素

你可以通过调用 contains() 方法来检查 Set 是否包含给定的元素。例如:

Set<String> set = new HashSet<>();
set.add("element 1");
set.add("element 2");
set.add("element 3");
System.out.println("是否包含 `element 2` 元素:" + set.contains("element 2"));
//是否包含 `element 2` 元素:true

要确定 Set 是否包含某个元素,Set 会在内部遍历所有元素,将每个元素与传入的参数对象进行比较。这种比较通过调用元素的 equals() 方法来判断两者是否相等。

由于 Set 允许添加 null 值,因此也可以检查集合中是否包含 null。例如:

Set<String> set = new HashSet<>();
set.add("element 1");
set.add("element 2");
set.add(null);
System.out.println("是否包含 `null` 元素:" + set.contains(null));
//是否包含 `null` 元素:true

显然,若传给 contains() 方法的参数是 null,该方法不会通过 equals() 方法与元素进行比较,而是直接使用 == 运算符来判断(因为 null 调用 equals() 会抛出空指针异常)。

  

Set 转换为 List

你可以通过创建一个 List 并调用其 addAll() 方法,将 Set 作为参数传入该方法,从而实现 Set 到 List 的转换。例如:

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

List<String> list = new ArrayList<>();
list.addAll(set);
System.out.println("list=" + list);
//list=[element 3, element 2, element 1]

运行这个示例后,List 将包含字符串元素 "element 3"、"element 2" 和 "element 1" —— 这是因为调用 List 的 addAll(set) 方法时,会将 Set 中所有现存元素都添加到 List 中。

  

  

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号