# 集合接口

> 原文： [https://docs.oracle.com/javase/tutorial/collections/interfaces/set.html](https://docs.oracle.com/javase/tutorial/collections/interfaces/set.html)

[`Set`](https://docs.oracle.com/javase/8/docs/api/java/util/Set.html) 是 [`Collection`](https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html) ，不能包含重复元素。它模拟了数学集抽象。 `Set`接口仅包含从`Collection`继承的方法，并添加了禁止重复元素的限制。 `Set`还为`equals`和`hashCode`操作的行为增加了一个更强的契约，允许`Set`实例有意义地进行比较，即使它们的实现类型不同。如果两个`Set`实例包含相同的元素，则它们是相等的。

Java 平台包含三个通用`Set`实现：`HashSet`，`TreeSet`和`LinkedHashSet`。 [`HashSet`](https://docs.oracle.com/javase/8/docs/api/java/util/HashSet.html) 将其元素存储在哈希表中，是性能最佳的实现;但它不能保证迭代的顺序。 [`TreeSet`](https://docs.oracle.com/javase/8/docs/api/java/util/TreeSet.html) 将其元素存储在红黑树中，根据其值对其元素进行排序;它比`HashSet`慢得多。 [`LinkedHashSet`](https://docs.oracle.com/javase/8/docs/api/java/util/LinkedHashSet.html) ，作为哈希表实现，其中链接列表贯穿其中，根据它们插入集合（插入顺序）的顺序对其元素进行排序。 `LinkedHashSet`使客户免于`HashSet`提供的未指定的，通常混乱的订单，成本仅略高。

这是一个简单但有用的`Set`成语。假设您有`Collection`，`c`，并且您想要创建另一个包含相同元素的`Collection`，但会删除所有重复项。下面的单线程就可以了。

```
Collection<Type> noDups = new HashSet<Type>(c);

```

它的工作原理是创建一个`Set`（根据定义，它不能包含重复项），最初包含`c`中的所有元素。它使用[集合接口](collection.html)部分中描述的标准转换构造函数。

或者，如果使用 JDK 8 或更高版本，您可以使用聚合操作轻松收集到`Set`：

```
c.stream()
.collect(Collectors.toSet()); // no duplicates

```

这是一个稍长的例子，它将`Collection`的名称累积到`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`。

以下 [`program`](examples/FindDups.java) 打印出其参数列表中的所有不同单词。提供了该程序的两个版本。第一个使用 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()+ 
                           " distinct words: " + 
                           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() + " distinct words: " + s);
    }
}

```

现在运行该程序的任一版本。

```
java FindDups i came i saw i left

```

生成以下输出：

```
4 distinct words: [left, came, saw, i]

```

请注意，代码始终通过其接口类型（`Set`）而不是其实现类型引用`Collection`。这是 _ 强烈 _ 推荐的编程实践，因为它使您可以灵活地仅通过更改构造函数来更改实现。如果用于存储集合的变量或用于传递它的参数被声明为`Collection`的实现类型而不是其接口类型，_ 所有 _ 这样的变量和参数必须是已更改以更改其实现类型。

此外，无法保证生成的程序能够正常运行。如果程序使用原始实现类型中存在但未在新实现类型中存在的任何非标准操作，则程序将失败。仅通过其界面引用集合可防止您使用任何非标准操作。

前面例子中`Set`的实现类型是`HashSet`，它不能保证`Set`中元素的顺序。如果您希望程序按字母顺序打印单词列表，只需将`Set`的实现类型从`HashSet`更改为`TreeSet`。进行这个简单的单行更改会导致前一个示例中的命令行生成以下输出。

```
java FindDups i came i saw i left

4 distinct words: [came, i, left, saw]

```

## 设置接口批量操作

批量操作特别适合`Set`;应用时，它们执行标准的集合代数运算。假设`s1`和`s2`是集合。以下是批量操作的作用：

*   `s1.containsAll(s2)` - 如果`s2`是`s1`的**子集**，则返回`true`。 （`s2`是`s1`的子集，如果设置`s1`包含`s2`中的所有元素。）
*   `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`程序。假设您想知道参数列表中的哪些单词只出现一次，哪些出现多次，但您不希望重复打印任何重复项。这种效果可以通过生成两个集合来实现 - 一个集合包含参数列表中的每个单词，另一个集合仅包含重复项目。仅出现一次的单词是这两组的集合差异，我们知道如何计算。这是 [`the resulting program`](examples/FindDups2.java) 的样子。

```
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);

        // Destructive set-difference
        uniques.removeAll(dups);

        System.out.println("Unique words:    " + uniques);
        System.out.println("Duplicate words: " + dups);
    }
}

```

当使用先前使用的相同参数列表（`i came i saw i left`）运行时，程序将生成以下输出。

```
Unique words:    [left, saw, came]
Duplicate words: [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`做任何特殊操作。这些操作在[收集接口](collection.html)部分中描述。