Set
是不能包含重复元素的集合。它为数学集合抽象建模。Set
接口仅包含从Collection
继承的方法,并增加了禁止重复元素的限制。Set
还为equals
和hashCode
操作的行为增加了更紧密的约定,允许Set
实例进行有意义的比较,即使它们的实现类型不同。如果两个Set
实例包含相同的元素,则它们相等。
Java平台包含三个泛型Set
实现:HashSet
,TreeSet
,和LinkedHashSet
。 HashSet
将其元素存储在哈希表中,是性能最佳的实现。但是,它不能保证迭代的顺序。 TreeSet
将其元素存储在红黑树中,根据其值对元素进行排序;它比HashSet
慢得多。 LinkedHashSet
实现为哈希表,并在其中运行链表,它根据元素插入到集合中的顺序(插入顺序)对元素进行排序。LinkedHashSet
将其客户从HashSet
提供的未指定的,通常是混乱的订购中省去了,但成本略高。
这是一个简单但有用的Set
习语。假设您有一个Collection c
,并且您想要创建一个包含相同元素但消除了所有重复项的另一个Collection
。下面的单行代码可以解决问题。
Collection<Type> noDups = new HashSet<Type>(c);
它通过创建一个Set
(根据定义不能包含重复项)来工作,该Set
最初包含c中的所有元素。 它使用“Collection接口”部分中描述的标准转换构造函数。
或者,如果使用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);
}
set接口基本操作
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()+
" 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
。强烈建议您使用这种编程方法,因为它使您仅通过更改构造函数即可灵活地更改实现。如果声明用于存储集合的变量或用于传递集合的参数中的任何一个被声明为集合的实现类型,而不是其接口类型,则必须更改所有此类变量和参数,以更改其实现类型。
此外,不能保证所生成的程序将正常运行。如果程序使用原始实现类型中存在的任何非标准操作,而不是新实现类型中的任何非标准操作,则该程序将失败。仅通过集合的接口引用集合可以防止您使用任何非标准操作。
上一示例中Set
的实现类型是HashSet
,它不能保证Set
中元素的顺序。如果要让程序按字母顺序打印单词列表,只需将Set
的实现类型从HashSet
更改为TreeSet
。进行此单行琐碎的更改会使上一个示例中的命令行生成以下输出。
java FindDups i came i saw i left
4 distinct words: [came, i, left, saw]
set接口批量操作
批量操作特别适合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);
// 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
所做的之外,对Set
并没有做任何特殊的事情。这些操作在“Collection接口”部分中进行了描述。