本教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,可能使用不再可用的技术。
请参阅Java语言变化,了解Java SE 9及其后续版本中更新的语言特性的摘要。
请参阅JDK发行说明,了解所有JDK版本的新功能、增强功能以及已删除或已弃用选项的信息。
Set
是一个不能包含重复元素的Collection
接口,它模拟了数学中的集合抽象概念。 Set
接口仅包含从 Collection
继承的方法,并添加了不允许重复元素的限制。 Set
还对 equals
和 hashCode
操作的行为添加了更强的约束,使得即使其实现类型不同,Set
实例也可以有意义地进行比较。如果两个 Set
实例包含相同的元素,则它们是相等的。
Java 平台包含三个通用的 Set
实现: HashSet
、TreeSet
和 LinkedHashSet
。在性能上,HashSet
是最好的实现,它将元素存储在哈希表中;然而,它不保证迭代顺序。 TreeSet
将元素存储在红黑树中,根据值的顺序对其进行排序;它的速度比 HashSet
慢得多。 LinkedHashSet
是一个以哈希表和链表为基础的实现,它根据元素插入的顺序(插入顺序)对元素进行排序。相比于 HashSet
提供的未指定的、通常是混乱的排序,LinkedHashSet
以稍微更高的成本为其客户端提供了有序的排序。
下面是一个简单但有用的 Set
用法。假设你有一个名为 c
的 Collection
,你想创建另一个不包含重复元素的 Collection
。以下一行代码可以实现这个目标。
Collection<Type> noDups = new HashSet<Type>(c);
它通过创建一个 Set
(根据定义,不能包含重复元素),最初包含 c
中的所有元素。它使用了在集合接口部分中描述的标准转换构造函数。
或者,如果使用 JDK 8 或更高版本,你可以使用聚合操作轻松地将其收集到一个 Set
中:
c.stream() .collect(Collectors.toSet()); // 无重复元素
下面是一个稍微更长的示例,将一组名称累积到一个 TreeSet
中:
Set<String> set = people.stream() .map(Person::getName) .collect(Collectors.toCollection(TreeSet::new));
下面是第一个用法的一个小变种,它保留了原始集合的顺序,同时删除重复元素:
Collection<Type> noDups = new LinkedHashSet<Type>(c);
下面是一个封装了上述用法的通用方法,返回一个与传入的泛型类型相同的Set
:
public static <E> Set<E> removeDups(Collection<E> c) { return new LinkedHashSet<E>(c); }
size
操作返回Set
中元素的数量(也称为基数)。isEmpty
方法的功能很容易理解。add
方法将指定的元素添加到Set
中,如果该元素已经存在,则返回一个布尔值指示是否添加了该元素。类似地,remove
方法从Set
中移除指定的元素,如果该元素存在,则返回一个布尔值指示该元素是否存在。iterator
方法返回一个Set
上的Iterator
。
下面的程序
输出其参数列表中的所有不同单词。提供了两个版本的程序。第一个版本使用了JDK 8的聚合操作。第二个版本使用了for-each循环。
使用JDK 8的聚合操作:
import java.util.*; import java.util.stream.*; public class FindDups { public static void main(String[] args) { Set<String> distinctWords = Arrays.asList(args).stream() .collect(Collectors.toSet()); System.out.println(distinctWords.size()+ "个不同的单词:" + distinctWords); } }
使用for-each
循环:
import java.util.*; public class FindDups { public static void main(String[] args) { Set<String> s = new HashSet<String>(); for (String a : args) s.add(a); System.out.println(s.size() + "个不同的单词:" + s); } }
现在运行程序的任何一个版本。
java FindDups i came i saw i left
输出如下:
4个不同的单词:[left, came, saw, i]
请注意,代码总是通过其接口类型(Set
)而不是其实现类型来引用Collection
。这是一种强烈推荐的编程实践,因为它使您可以通过仅更改构造函数来灵活地更改实现。如果用于存储集合的变量或用于传递集合的参数声明为Collection
的实现类型而不是接口类型,则必须更改所有这些变量和参数才能更改其实现类型。
此外,并不能保证生成的程序能够正常运行。如果程序使用了原始实现类型中存在但新实现中不存在的非标准操作,程序将会失败。仅通过接口来引用集合,可以防止使用任何非标准操作。
前面示例中Set
的实现类型是HashSet
,它不能保证Set
中元素的顺序。如果希望程序按字母顺序打印单词列表,只需要将Set
的实现类型从HashSet
改为TreeSet
。做出这个简单的一行更改后,上一个示例中的命令行将生成以下输出。
java FindDups i came i saw i left 4 个不同的单词: [came, i, left, saw]
批量操作非常适用于Set
;应用这些操作时,它们执行标准的集合代数操作。假设s1
和s2
是集合。下面是批量操作的作用:
s1.containsAll(s2)
— 如果s2
是s1
的子集,则返回true
。(如果集合s1
包含s2
中的所有元素,则s2
是s1
的子集。)s1.addAll(s2)
— 将s1
转换为s1
和s2
的并集。(两个集合的并集是包含两个集合中所有元素的集合。)s1.retainAll(s2)
— 将s1
转换为s1
和s2
的交集。(两个集合的交集是只包含两个集合共有元素的集合。)s1.removeAll(s2)
— 将s1
转换为s1
和s2
的(非对称)差集。(例如,s1
减去s2
的差集是包含在s1
中但不在s2
中的所有元素的集合。)要对两个集合进行并集、交集或差集的计算而不改变任何集合(即非破坏性操作),调用者必须在调用相应的批量操作之前复制一个集合。以下是相应的习惯用法。
Set<Type> union = new HashSet<Type>(s1); union.addAll(s2); Set<Type> intersection = new HashSet<Type>(s1); intersection.retainAll(s2); Set<Type> difference = new HashSet<Type>(s1); difference.removeAll(s2);
前述习惯用法中结果Set
的实现类型是HashSet
,正如前面提到的,它是Java平台上最全面的Set
实现。但是,可以用任何通用的Set
实现来替代。
让我们重新审视一下FindDups
程序。假设您想知道参数列表中出现一次的单词和出现多次的单词,但不想重复打印任何重复的单词。可以通过生成两个集合来实现这个效果——一个包含参数列表中的每个单词,另一个只包含重复的单词。只出现一次的单词是这两个集合的差集,我们知道如何计算。下面是生成的程序
的样子。
import java.util.*; public class FindDups2 { public static void main(String[] args) { Set<String> uniques = new HashSet<String>(); Set<String> dups = new HashSet<String>(); for (String a : args) if (!uniques.add(a)) dups.add(a); // 破坏性的集合差 uniques.removeAll(dups); System.out.println("唯一的单词: " + uniques); System.out.println("重复的单词: " + dups); } }
当使用先前使用的相同参数列表运行时(i came i saw i left
),该程序产生以下输出。
唯一的单词: [left, saw, came] 重复的单词: [i]
一个不太常见的集合代数操作是对称差——两个指定集合中包含的元素中不包含在两者中的。以下代码以非破坏性的方式计算两个集合的对称差。
Set<Type> symmetricDiff = new HashSet<Type>(s1); symmetricDiff.addAll(s2); Set<Type> tmp = new HashSet<Type>(s1); tmp.retainAll(s2); symmetricDiff.removeAll(tmp);
数组操作对于Set
与对于任何其他Collection
并无特殊之处。有关这些操作的描述,请参见集合接口部分。