Set介面
Set是一個不能包含重複元素的Collection,它模擬了數學集抽象,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
(根據定義,它不能包含重複項),初始化包含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]
請注意,程式碼始終引用Collection
通過其介面型別(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
是Set
,批量操作是這樣做的:
-
s1.containsAll(s2
) — 如果s2
是s1
的子集,則返回true
(如果set
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);
前面的語法中的結果集的實現型別是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介面部分介紹了這些操作。